Grokking Advanced Go
Vote

0% completed

​

Value and Pointer Semantics

Value Semantics and Pointer Semantics

Copying a Struct That Holds a Slice

Choosing a Receiver Kind

Pointer Speed

Types That Behave Like References

Using This in an Interview

Key Takeaways

The two functions below receive the same Order, and both contain the same assignment. After the first call, the caller's order is still unpaid. After the second call, the caller's order is paid.

Go
Go

. . . .

Output:

after markPaidCopy: false after markPaid: true

The only difference is one character in the parameter type, yet only one of the two functions can change the caller's order. What exactly does each kind of parameter copy, what happens when the copied struct contains a slice, and are pointers really faster? The answers also show how to choose a receiver kind.

Value Semantics and Pointer Semantics

A holder is anything that stores data, such as a variable, a field, or a parameter. When each holder of a piece of data has its own independent copy (value semantics), a change that one holder makes is invisible to all the others. When several holders refer to one piece of data (pointer semantics), a change made through any of them is visible to all of them.

Go passes every argument by copying it. The function markPaidCopy receives a copy of the whole struct, so its assignment changes only that copy. The function markPaid also receives a copy, but what it copies is an address. Both the caller and the function then hold the address of the same Order. The assignment therefore changes the one value they share.

Copying a Struct That Holds a Slice

Value semantics apply only to the bytes that are actually copied, and for a slice field those bytes are not the elements. A slice value is a small header made of a pointer to an array, a length, and a capacity. Copying an Order copies that header, and both copies then point to the same array of items.

The next function changes three things through a parameter that looks like a fully independent copy.

Go
Go

. . . .

Output:

customer: Ana first quantity: 5 number of items: 1

The three results have three different causes:

  1. The customer is unchanged because the string field belongs to the copy.
  2. The quantity did change because o.Items[0] refers to the array that both headers point to.
  3. The number of items is unchanged because append returned a new header, and only the Items field of the copy received it.

A struct copy is shallow. It duplicates the fields themselves, but it does not duplicate anything those fields point to. This result surprises many engineers: a function that takes Order by value can still change the caller's items. If a function must change its items without affecting the caller, it has to copy them first with slices.Clone.

Copying an Order copies the slice header
Copying an Order copies the slice header

Choosing a Receiver Kind

A method receiver follows the same rules as a parameter. A value receiver gets a copy of the value, and a pointer receiver gets its address. The Go FAQ lists three considerations, in this order:

  • Does the method need to change the receiver? If so, use a pointer receiver.
  • Is the value large? If so, a pointer receiver may be cheaper.
  • Do other methods of the type have pointer receivers? If so, use a pointer receiver here as well, so that the type has one kind throughout.

A struct that must not be copied, such as one that contains a lock (sync.Mutex), also needs a pointer receiver. A value receiver is a good fit for a small type that behaves like a single value. An example is time.Time, whose Add method returns a new time rather than changing the old one.

Keeping one kind per type matters because the two kinds differ in the method set (the list of methods a type has for the purpose of implementing an interface). Methods with pointer receivers belong to *Order but not to Order, so the code below does not compile.

type Payable interface { MarkPaid() } func (o *Order) MarkPaid() { o.Paid = true } var p Payable = Order{ID: 1001}
./main.go:14:17: cannot use Order{…} (value of struct type Order) as Payable value in variable declaration: Order does not implement Payable (method MarkPaid has pointer receiver)

A mix of kinds on one type means that some methods are in the method set of Order while others are only in the method set of *Order. It also means that some methods work on a copy while others work on the shared value. The reader then has to check each method to find out which.

Pointer Speed

Many engineers pass pointers everywhere because they expect that avoiding a copy saves time. The program below answers two separate questions about an Order, which is 56 bytes on amd64: what passing it costs, and what returning it costs. A 4 KB array is included for comparison. The //go:noinline comments keep each call in place, so that the program measures the calls themselves.

Go
Go

. . . .

One possible output (measured on one machine):

pass value 2.5 ns per call pass pointer 2.1 ns per call pass 4 KB value 50.4 ns per call return value 3.8 ns per call return pointer 17.4 ns per call

Passing a 56-byte struct by value costs less than a nanosecond more than passing a pointer on this machine. Returning a pointer costs several times more than returning a value. The Order must then be placed in memory that outlives the call (an allocation), and the garbage collector (the part of the runtime that frees unused memory) must later find it and free it.

"Pointers are faster" is often false. A pointer avoids a copy, but it can force an allocation, and an allocation usually costs more than copying a few dozen bytes. The 4 KB line shows the opposite case. A copy does become expensive once the value is large enough, so size is a fair reason to choose a pointer. Choose by meaning first, and change that choice only where a measurement shows a real cost.

Types That Behave Like References

Slices and maps are often described as reference types, and that description is only partly true. A map value is one pointer to a runtime structure, and a slice value is a header that holds a pointer. Both are still copied when passed, and the program below shows what that copy can and cannot do.

Go
Go

. . . .

Output:

map[A-100:5] [1250 799] map bytes: 8 slice bytes: 24

A function can change what the map or the slice elements contain, and the caller sees that change. A function cannot change the caller's variable, so assigning a new map, or a slice with a new length, has no effect outside the function. To change the caller's slice, a function returns the new slice or takes a pointer to it. The append function itself follows the first of these patterns.

TypeWhat a call copiesFunction can change caller's dataFunction can change caller's variable
StructEvery fieldOnly through pointer, slice, or map fieldsNo
ArrayEvery elementNo, unless elements hold pointersNo
PointerThe addressYesOnly by taking a pointer to the pointer
SlicePointer, length, capacityYes, the elementsNo, not its length or capacity
MapOne pointerYes, the entriesNo
StringPointer and lengthNo, strings cannot changeNo

Using This in an Interview

Say that Go always passes by copying. A pointer or slice has a small value to copy, and that value still refers to shared data. Use a pointer receiver when a method changes the value or the value must not be copied, and keep one receiver kind per type. A likely follow-up asks why a function that takes a struct by value can still change the caller's data. The struct may contain a slice, map, or pointer, and the copy refers to the same underlying data.

Key Takeaways

  • Go passes every argument by copying it; a pointer parameter copies an address, so both sides share one value.
  • A struct copy is shallow, so a copied Order still shares its array of items with the original.
  • Use a pointer receiver when a method changes the value or the value must not be copied, and keep one receiver kind per type.
  • Passing a pointer saves little for a small struct, and returning one can force an allocation that costs far more than a copy.
  • Slices and maps share their contents when copied, but a function still cannot change the caller's variable.

Every assignment and every call in Go makes a copy, and the only question is what that copy contains. A copy that contains the data gives each holder its own value. A copy that contains an address gives all holders one shared value. Once you can name which of the two a type gives you, you can predict what each function is able to change.

Reading Progress

0%


Vote for new content

On This Page

Value Semantics and Pointer Semantics

Copying a Struct That Holds a Slice

Choosing a Receiver Kind

Pointer Speed

Types That Behave Like References

Using This in an Interview

Key Takeaways