Pocket Gophers

Why Use Pointers?

go1.9

If you are a new programmer or coming to Go from a language without pointers like Python, you might wonder just why you should use pointers. By knowing why pointers are used, you will also know when to use them.

The main reasons to use pointers are:

I demonstrate each of these reasons with working code.

First, a warning:

Be mindful that passing pointers around can cause unintended and difficult to detect problems, for exactly the reason stated: the value of the underlying structure can be changed by any function that holds a reference to the pointer. It is generally safer to pass variables by value rather by reference, unless mandated by an interface, or when memory optimization is necessary.
/u/kor_the_fiend

The code for these examples can be obtained with:

go get -d pocketgophers.com/why-use-pointers

Assign a variable from another function

Pointers can be used to set a variable from anther function, such as is done by json.Unmarshal and the flag package.

Attempting to reset the local variable value from another function demonstrates how this works:

another_function.go
package main

import "fmt"

func reset(value int) {
	value = 0
}

func resetPtr(value *int) {
	*value = 0
}

func main() {
	value := 1
	fmt.Println("initial", value)

	reset(value)
	fmt.Println("after reset", value)

	resetPtr(&value)
	fmt.Println("after resetPtr", value)
}

Outputs:

$ go run another_function.go
initial 1
after reset 1
after resetPtr 0

Notice that only resetPtr changes value in main.

Modify a variable from a member function

The follows from the previous example. The only differences are that the function is a member function and the value changed is myType.value.

member_function.go
package main

import "fmt"

type myType struct {
	value int
}

func (mt myType) reset() {
	mt.value = 0
}

func (mt *myType) resetPtr() {
	mt.value = 0
}

func main() {
	mt := myType{
		value: 1,
	}
	fmt.Println("initial", mt.value)

	mt.reset()
	fmt.Println("after reset", mt.value)

	mt.resetPtr()
	fmt.Println("after resetPtr", mt.value)
}

Outputs:

$ go run member_function.go
initial 1
after reset 1
after resetPtr 0

Again, only resetPtr changes value in main.

Manage copy on function call

All functions calls are pass by value.

… the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.
Go Specification: Calls

What this means in practice is that if a variable is passed to a function, the function will work on a copy of the value of the variable. As a result, the function cannot change the original variable. If the passed value is a pointer, the function can change the value pointed at, but not the variable that was passed in.

Other than controlling access, these copies use memory. This is only a problem when the value being copied is large. To show the effects of this, I will build an example that explores alternatives with go run. The first thing we need is a large value:

BigStruct struct {
	value int
	stuff [1000000]int
}

The alternatives need to create an instance of BigStruct or *BigStruct and have a function that can be called using the same type. Here is the alternative for BigStruct:

value.go
package main

func create() BigStruct {
	return BigStruct{}
}

func call(b BigStruct) {
	_ = b.value
}

I use benchmarks to make the comparisons:

main.go
package main

import (
	"fmt"
	"testing"
)

type BigStruct struct {
	value int
	stuff [1000000]int
}

func main() {
	// benchmark multiple times for benchstat
	for i := 0; i < 5; i++ {
		result := testing.Benchmark(func(b *testing.B) {
			big := create()

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				call(big)
			}
		})

		fmt.Printf("BenchmarkCall\t%s\t%s\n",
			result, result.MemString())
	}
}

The alternative for *BigStruct is:

pointer.go
package main

func create() *BigStruct {
	return &BigStruct{}
}

func call(b *BigStruct) {
	_ = b.value
}

With these two alternatives, we can run them and compare the results with benchstat, which tells us if the differences are significant:

$ go run main.go value.go > value.txt
$ go run main.go pointer.go > pointer.txt
value.txtpointer.txt
time/opdelta
Call7.64ms ±75%0.00ms ±23%−100.00%(p=0.008 n=5+5)
 
alloc/opdelta
Call0.00B 0.00B ~(all equal)
 
allocs/opdelta
Call0.00 0.00 ~(all equal)
 

The delta column shows the difference between the two benchmarks. A delta of ~ means there is no significant difference. Negative numbers mean the second alternative is better. Positive numbers mean the second alternative is worse. (Assuming you are trying to minimize the values.)

In this case we can see the pointer version is 100% faster (time/op, op means operation). There is no significant difference in how much memory (alloc/op) or how many allocations are made (allocs/op).

Although no memory is allocated, there is a significant difference in time. The time difference can be attributed to copying b from the calling function’s stack to the called function’s stack.

To get the memory difference to show up in the memory and allocation results, the escape analyzer needs to move b from the stack to the heap. I forced this by accessing b from a goroutine:

value-escapes.go
package main

func create() BigStruct {
	return BigStruct{}
}

func call(b BigStruct) {
	go func() {
		_ = b.value
	}()
}

You can find out what escapes with -gcflags=-m:

$ go build -gcflags=-m main.go value-escapes.go
# command-line-arguments
./value-escapes.go:3:6: can inline create
./value-escapes.go:8:5: can inline call.func1
./main.go:17:17: inlining call to create
./value-escapes.go:8:5: func literal escapes to heap
./value-escapes.go:8:5: func literal escapes to heap
./value-escapes.go:9:7: &b escapes to heap
./value-escapes.go:7:13: moved to heap: b
./main.go:16:31: func literal escapes to heap
./main.go:16:31: func literal escapes to heap
./main.go:25:14: result escapes to heap
./main.go:26:28: result.MemString() escapes to heap
./main.go:25:13: main ... argument does not escape
./main.go:16:39: main.func1 b does not escape

I used go build here to avoid running the code. As desired, b escapes. This alternative shows:

$ go run main.go value-escapes.go > value-escapes.txt
value-escapes.txtpointer.txt
time/opdelta
Call18.5ms ± 6%0.0ms ±23%−100.00%(p=0.008 n=5+5)
 
alloc/opdelta
Call8.00MB ± 0%0.00MB −100.00%(p=0.000 n=4+5)
 
allocs/opdelta
Call1.00 ± 0%0.00 −100.00%(p=0.008 n=5+5)
 

Now the difference in memory use is clear. Passing a pointer uses less memory and is faster than passing BigStruct directly.

I forced b to escape so the memory use would appear in the benchmark. Normally this is something that should not be done as memory allocated in the heap needs to be garbage collected. Memory that stays in the stack will be freed when the function returns and its stack is freed. Notice how forcing b to escape reduces performance:

value.txtvalue-escapes.txt
time/opdelta
Call7.64ms ±75%18.54ms ± 6%+142.78%(p=0.008 n=5+5)
 
alloc/opdelta
Call0.00B 8003586.25B ± 0%+Inf%(p=0.016 n=5+4)
 
allocs/opdelta
Call0.00 1.00 ± 0%+Inf%(p=0.008 n=5+5)
 

Dig into the Fundamentals of Go

Subscribe to receive a weekly email covering a Go fundamental. Be it the language, its tooling, or its packages, you will learn what you need to know.