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:
- To assign a variable from another function
- To modify a variable from a member function
- To manage copy on function call
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.
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:
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.
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.
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:
package main func create() BigStruct { return BigStruct{} } func call(b BigStruct) { _ = b.value }
I use benchmarks to make the comparisons:
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:
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.txt | pointer.txt | |||
|---|---|---|---|---|
| time/op | delta | |||
| Call | 7.64ms ±75% | 0.00ms ±23% | −100.00% | (p=0.008 n=5+5) |
| alloc/op | delta | |||
| Call | 0.00B | 0.00B | ~ | (all equal) |
| allocs/op | delta | |||
| Call | 0.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:
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.txt | pointer.txt | |||
|---|---|---|---|---|
| time/op | delta | |||
| Call | 18.5ms ± 6% | 0.0ms ±23% | −100.00% | (p=0.008 n=5+5) |
| alloc/op | delta | |||
| Call | 8.00MB ± 0% | 0.00MB | −100.00% | (p=0.000 n=4+5) |
| allocs/op | delta | |||
| Call | 1.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.txt | value-escapes.txt | |||
|---|---|---|---|---|
| time/op | delta | |||
| Call | 7.64ms ±75% | 18.54ms ± 6% | +142.78% | (p=0.008 n=5+5) |
| alloc/op | delta | |||
| Call | 0.00B | 8003586.25B ± 0% | +Inf% | (p=0.016 n=5+4) |
| allocs/op | delta | |||
| Call | 0.00 | 1.00 ± 0% | +Inf% | (p=0.008 n=5+5) |