Creating new data types is an important part of the work of every programmer. In most languages, a type definition consists of a description of its fields and methods. In Golang, in addition to this, you need to decide which recipient semantics for methods of the new type to use: value (value) or pointer (pointer). At first glance, this decision may seem secondary, because in most cases the program will work with any semantics of the recipient. Therefore, many people miss this point and write the code, and have not figured it out to the end, which is affected by the semantics of the method recipient. And to figure it out, you need to go a little deeper into how Golang works.
Consider a small example. Define a
cat structure with one
Name field and
sayHello (person string) method. Hereinafter,
by a method I will refer to a function associated with a particular type, an
object to a variable that has methods, and the
recipient of the method will be the variable indicated in parentheses after the word
func in the method description.
type cat struct { Name string } func (c *cat) sayHello(person string) { fmt.Println(fmt.Sprintf("Meow, meow, %s!", person) }
If we define a pointer to
cat and request the
Name field from it, then, obviously, we will get an error, since the field is called from
nil :
var c *cat
https://play.golang.org/p/L3FnRJXKqs0
However, when the
sayHello () method is called on the same variable, there will be no error:
var c *cat
https://play.golang.org/p/EMoFgKL1HEi
Why can
nil call a method in this example, and how is this explained in terms of the architecture of the language itself? This becomes possible because the method in Go is syntactic sugar, or, in other words, a wrapper around a function that has a receiver as one of its arguments. When the
c.sayHello (“Human”) method is called, the
(* cat) .sayHello (c, s) construct (
https://play.golang.org/p/X9leJeIvxcA ) will actually be called. By calling the
nil method from the example above, we practically call the function with
nil in the arguments, and this is already quite a normal situation. Therefore, in Go
nil, it is the correct recipient for methods.
Since the method receiver is actually an argument, the recommendations for using the semantics “value” or “pointer” for the method receiver are similar to the recommendations for function arguments. They, in turn, are inferred from Go's basic rule:
arguments are always passed to the function by value . This means that the transfer of any argument to the function occurs through its copying: if the function accepts a structure as an input, then a full copy of this structure will come inside it; if it takes a pointer to an object, then a new variable will come with a pointer to the same object. This can be seen by comparing the variable address before passing it to the function with the address of the argument inside the function (
https://play.golang.org/p/oc2ssC_Irs8 ,
https://play.golang.org/p/FeQa2HUdX0a ).
When link passing is used:
- For large structures. The pointer occupies only one machine word (32, 64 bits depending on the system). Therefore, when calling a method with a pointer in the receiver, copying the pointer is cheaper than copying the entire object, as would be the case by passing by value.
- If the called method modifies the data of the object itself. When the recipient is passed by reference, the method can affect the state of the calling object by indirectly making changes. Which is impossible when passing by value.
When using value transfer:
- For simple built-in types such as numbers, strings, bool. When using the pointer, almost the same amount of memory is used as the object of this type has, and the cost of its maintenance by the garbage collector increases, as will be described below.
- For slices, as well as other reference types: map and channels - it makes no sense to take a pointer. They themselves are already a pointer.
- With multithreading, passing by value is safe, unlike passing by reference.
- For small structures. In such cases, the transmission by value is more efficient. This is because the internal data of the methods are placed in a separate frame of the stack. After exiting a function, its frame is cleared. When we rummage something along the pointer, we transfer this data from the stack to the heap, from where this data may be available for other functions. Increasing the heap creates an additional burden on the garbage collector, whose operation reduces the program speed by an average of 25%. When using value-by-value transfer, the data remains on the stack and no additional garbage collector work is required.
When you need to think about the semantics of the recipient:
- The type of recipient may vary by subject area. In one of his speeches, Bill Kennedy gave a good example with the user type describing the user. When passed by value, a copy will be created for user. This will lead to the fact that several copies of the same user can coexist in the program at the same time, which can then be independently changed, which does not correspond to the subject area, because the real user is always one, and he cannot be described at different times by different sets data.
- Another sure way to determine the type of recipient for a method is to use the constructor method for its type. If the constructor returns a value / pointer, then when creating an entity, it is assumed that they will continue to work with it as a value / pointer. Therefore, it is also better to use the same semantics in the method receiver.
- There is an unwritten rule, in violation of which the compiler will not swear, but your code will definitely not get better from this. If one of the type methods uses a pointer / value as the receiver, then to maintain consistency, the remaining methods must use a pointer / value. Type methods should not have a hash of value- and pointer-receivers.
What is the result
In Go Value, semantics means copying a value; pointer semantics means giving access to a value. This applies both to the arguments of the methods and to their recipients. For built-in types, such as numbers, lines, slices, maps, channels, and small structures, you almost always need to use value-based transfer. For structures that occupy a large amount of memory, and structures whose state can be indirectly changed by their methods, you need to use the transfer by reference. Also, the semantics of the recipient may depend on the domain that the type describes, the semantics returned in its factory, and the semantics of the recipient already used in other methods of this type.