Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5. Pointers and Values

This is the chapter that changes how you read Go. In a scripting language the question “is this a copy or the same object?” usually has one answer for the whole language and you stop asking it. In Go it has a different answer per type, the answer is visible in the code once you know where to look, and getting it wrong produces a program that runs and is subtly incorrect.

The rule underneath is short: assignment copies the value, and the value is whatever the type says it is. Everything below is that rule applied.

5.1 What Copying Means

// What gets copied when you assign, and what does not.
package main

import "fmt"

type Point struct{ X, Y int }

func main() {
	// Structs copy.
	a := Point{1, 2}
	b := a
	b.X = 99
	fmt.Println(a, b) // {1 2} {99 2}

	// Arrays copy, including their elements.
	xs := [3]int{1, 2, 3}
	ys := xs
	ys[0] = 99
	fmt.Println(xs, ys) // [1 2 3] [99 2 3]

	// Slices copy the header, which points at shared elements.
	ss := []int{1, 2, 3}
	ts := ss
	ts[0] = 99
	fmt.Println(ss, ts) // [99 2 3] [99 2 3]
}
{1 2} {99 2}
[1 2 3] [99 2 3]
[99 2 3] [99 2 3]

A struct is its fields, so copying one copies every field, and b is a separate Point. An array is its elements, so copying a [3]int copies three integers, and ys is a separate array. Neither of these is how most languages behave and both follow from the rule.

A slice is not its elements. A slice value is a small header — a pointer to an array, a length, and a capacity — so copying a slice copies the header and leaves both copies pointing at the same elements. ts[0] = 99 therefore changes what ss sees. The slice was copied faithfully; the copy just happens to contain a pointer.

Maps and channels work the same way, and so, of course, do pointers. When someone calls these “reference types” that is what they mean: not that Go treats them specially, but that the value being copied has an address inside it. There is no special case in the language, so you can reason about it.

5.2 Pointers

&x is the address of x. *p is the value at p. The type of &x when x is an int is *int.

// Taking an address, following it, and passing one to a function.
package main

import "fmt"

type Counter struct{ N int }

// byValue gets a copy and cannot change the caller's Counter.
func byValue(c Counter) { c.N++ }

// byPointer gets an address and can.
func byPointer(c *Counter) { c.N++ }

func swap(a, b *int) { *a, *b = *b, *a }

func main() {
	c := Counter{}
	byValue(c)
	fmt.Println(c.N) // 0
	byPointer(&c)
	fmt.Println(c.N) // 1

	x, y := 1, 2
	swap(&x, &y)
	fmt.Println(x, y) // 2 1

	p := &Counter{N: 10} // address of a composite literal
	q := new(Counter)    // pointer to a zeroed Counter
	fmt.Println(p.N, q.N, *p, *q)
}
0
1
2 1
10 0 {10} {0}

byValue receives a copy of the Counter and increments the copy, which the caller never sees. byPointer receives the address and increments what it points at. Go has no call by reference; passing a pointer is the only way to let a function reach the caller’s value.

Notice c.N++ inside byPointer, where c is a *Counter. Go dereferences pointers to structs automatically for field access, so c.N means (*c).N and nobody writes the longer form. This convenience is why Go code has far fewer visible * than C.

&Counter{N: 10} takes the address of a composite literal, idiomatic in Go and with no C equivalent. new(T) allocates a zeroed T and returns a *T; it is the same thing as &T{} and you will see both.

What Go does not have is pointer arithmetic. You cannot add to a pointer, walk one through an array, or cast one type of pointer to another. A pointer either points at a valid object of its type or is nil. That is what lets the garbage collector work and what rules out the family of buffer-overflow bugs. (The unsafe package can do these things. It is for people writing runtimes and it is not part of this book.)

5.3 Nil

The zero value of every pointer is nil, and following one stops the program.

// Following a nil pointer stops the program.
package main

import "fmt"

type Counter struct{ N int }

func main() {
	defer func() {
		fmt.Println("recovered:", recover())
	}()

	var p *Counter
	fmt.Println("p is nil:", p == nil)
	fmt.Println(p.N)
}
p is nil: true
recovered: runtime error: invalid memory address or nil pointer dereference

Without the recover, that is a crash with a stack trace. Coming from Lux this is the change that deserves attention: Lux has no null, and Go’s most common runtime failure is the one Lux removed. Go’s answer is not a type-system guarantee but a set of habits — return an error alongside a pointer rather than a nil pointer meaning failure, make the zero value of your types useful so nobody needs a pointer to express “not set yet”, and check at the boundary where a value enters your code rather than at every use. Chapter 8 is where those habits get written down.

nil is also the zero value of slices, maps, channels, functions, and interfaces, and it means something slightly different for each. A nil slice behaves like an empty one for len, range, and append; a nil map reads like an empty one and panics on write; a nil channel blocks forever. Those are in Chapters 7 and 11.

5.4 When To Use a Pointer

Three reasons, and no others worth the trouble.

The function must modify the caller’s value. byPointer above. If a function’s job is to change something, it takes a pointer to it.

The value must not be copied. A sync.Mutex protects the struct it lives in, and a copy of the struct has a second, independent mutex protecting nothing. Types like this are documented as “must not be copied”, and go vet enforces it for the ones it knows:

vet/copylock/main.go:13:13: show passes lock by value: goexamples/errors/vet/copylock.Registry contains sync.Mutex
vet/copylock/main.go:17:7: call of show copies lock value: goexamples/errors/vet/copylock.Registry contains sync.Mutex

The value is large. Copying a struct with thirty fields on every call costs something. In practice this reason is invoked far more often than it is measured; a struct of a handful of words copies faster than the pointer chase that avoiding the copy would cost, so let this one wait for a profiler.

The reason not to use a pointer is that a value cannot be nil, so a function taking a value has one less thing to check and one less way to fail. Prefer values, and reach for a pointer when one of the three reasons applies.

5.5 Where Values Live

You never free anything. Go is garbage collected, and a value stays alive as long as something refers to it.

You also do not choose between the stack and the heap. The compiler decides, by asking whether a value outlives the function that made it, and that analysis is why return &Counter{} is safe in Go where the equivalent in C returns a dangling pointer.

// Whether a value outlives its function decides where the compiler puts it.
package main

import "fmt"

type Big struct{ a [64]int }

func escapes() *Big { return &Big{} }

func stays() Big { return Big{} }

func main() {
	fmt.Println(len(escapes().a), len(stays().a))
}

go build -gcflags=-m prints the reasoning:

$ go build -gcflags=-m ./ch05/escape
ch05/escape/main.go:8:30: &Big{} escapes to heap

The Big made inside escapes outlives the call, so it goes on the heap; the one stays returns by value does not, so it does not. Read this output when you are chasing an allocation and ignore it otherwise. The practical consequence is only that taking the address of a local variable is always safe, so you can write the clear thing and let the compiler place it.

Exercise 5-1. Write func zero(xs []int) that sets every element to 0, and func zeroArray(xs [5]int) that tries to do the same. Call both and explain the difference without using the word “reference”.

Exercise 5-2. Write a linked list of integers with type node struct { val int; next *node }, plus functions to push at the front and to print the list.

Exercise 5-3. Write a function that takes a *int and returns without touching it, then call it with nil. Then write one that dereferences it. Note which of the two the compiler complains about.