When to Use Pointers in Go
Table of Contents
Pointers are variables that store memory addresses. Use them when code needs to update an original value or when nil matters, not because you assume they're faster. If performance is the reason you're considering a pointer, measure the code first.
All the content from our Boot.dev courses are available for free here on the blog. This one is the "Pointers" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!
When Should I Use a Pointer?
There are probably many nuanced cases for when a pointer is a good idea, but 90% of the time when you use a pointer it should be for one of the following reasons.
1. A Function That Mutates One of Its Parameters
When I call a function that takes a pointer as an argument, I expect that my variable will be mutated. If you aren't mutating the variable in your function, then you probably shouldn't be using a pointer.
2. Better Performance
If you have a string that contains an entire novel in memory it gets really expensive to copy that variable each time it is passed to a new function. It may be worthwhile to pass a pointer instead, which will save CPU and memory. This comes at the cost of readability however so only make this optimization if you must.
3. Need a Nil Value Option
Sometimes a function needs to know what something's value is, as well as if it exists or not. I usually use this when decoding JSON to know if a field exists or not. For example:
type Person struct {
Name *string `json:"name"`
}
func main(){
p := Person{}
json.Unmarshal([]byte(`{"name": "boot.dev"}`), &p)
fmt.Println(*p.Name) // prints "boot.dev"
json.Unmarshal([]byte(`{"name": ""}`), &p)
fmt.Println(*p.Name) // prints ""
json.Unmarshal([]byte(`{}`), &p)
fmt.Println(p.Name) // prints "<nil>"
}
If you are unsure, and a normal value will work just fine, I would advise avoiding the pointer. Pointers are useful tools but can lead to nasty bugs or unreadable code quite easily.
A Pointer Is a Variable
A variable is a named location in memory that stores a value. We can manipulate the value of a variable by assigning a new value to it or by performing operations on it. When we assign a value to a variable, we are storing that value in a specific location in memory.
A pointer is a variable that stores the memory address of another variable. This means that a pointer "points to" the location of where the data is stored, not the actual data itself.
The * syntax defines a pointer:
var p *int
The & operator generates a pointer to its operand.
myString := "hello"
myStringPtr := &myString
Nil Pointers and Dereferencing
It's possible to define an empty pointer. For example, an empty pointer to an integer:
var p *int
fmt.Printf("value of p: %v\n", p)
// value of p: <nil>
Its zero value is nil, which means it doesn't point to any memory address. Empty pointers are also called "nil pointers".
Instead of starting with a nil pointer, it's common to use the & operator to get a pointer to its operand:
myString := "hello" // myString is just a string
myStringPtr := &myString // myStringPtr is a pointer to myString's address
fmt.Printf("value of myStringPtr: %v\n", myStringPtr)
// value of myStringPtr: 0x140c050
The * operator dereferences a pointer to get the original value.
*myStringPtr = "world" // set myString through the pointer
fmt.Printf("value of myString: %s\n", *myStringPtr) // read myString through the pointer
// value of myString: world
Unlike C, Go has no pointer arithmetic.
Pointers can be very dangerous. If a pointer points to nothing, then dereferencing it will cause a runtime error called a panic that crashes the program. If nil is a valid input, check for it before trying to dereference the pointer.
Passing Pointers to Functions
Functions in Go pass variables by value, meaning that functions receive a copy of each argument:
func increment(x int) {
x++
fmt.Println(x)
// 6
}
func main() {
x := 5
increment(x)
fmt.Println(x)
// 5
}
The main function still prints 5 because the increment function received a copy of x.
One of the most common uses for pointers in Go is to pass a pointer to a function. The pointer itself is copied, but it contains the address of the original variable. This allows the function to update the original variable's value.
func increment(x *int) {
*x++
fmt.Println(*x)
// 6
}
func main() {
x := 5
increment(&x)
fmt.Println(x)
// 6
}
Fields of Pointers
When your function receives a pointer to a struct, you might try to access a field like this and encounter an error:
msgTotal := *analytics.MessagesTotal
Instead, access it like you'd normally do using a selector expression.
msgTotal := analytics.MessagesTotal
This approach is the recommended, simplest way to access struct fields in Go, and is shorthand for:
(*analytics).MessagesTotal
Pointer Receivers
A receiver type on a method can be a pointer.
Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are more common than value receivers. You can call a pointer-receiver method on an addressable value; Go automatically derives the pointer.
Pointer Receiver
type car struct {
color string
}
func (c *car) setColor(color string) {
c.color = color
}
func main() {
c := car{
color: "white",
}
c.setColor("blue")
fmt.Println(c.color)
// prints "blue"
}
Non-Pointer Receiver
type car struct {
color string
}
func (c car) setColor(color string) {
c.color = color
}
func main() {
c := car{
color: "white",
}
c.setColor("blue")
fmt.Println(c.color)
// prints "white"
}
The non-pointer receiver example prints "white" instead of "blue" because the method receives a copy of the struct. Without using a pointer receiver, any changes made inside the method only affect that copy, not the original.
Pointer Performance
Occasionally, new Go developers hear "pointers don't pass copies" and take that to a logical extreme, concluding:
Pointers are always faster because copying is slow. I'll always use pointers!
No. Bad. Stop.
Here are my rules of thumb:
- First, worry about writing clear, correct, maintainable code.
- If you have a performance problem, fix it.
Before even thinking about using pointers to optimize your code, use pointers when you need a shared reference to a value; otherwise, just use values.
If you do have a performance problem, consider:
- Stack vs. Heap
- Copying
Interestingly, local non-pointer variables are generally faster to pass around than pointers because they're stored on the stack, which is faster to access than the heap. Even though copying is involved, the stack is so fast that it's no big deal.
Once the value becomes large enough that copying is the greater problem, it can be worth using a pointer to avoid copying. Taking a pointer doesn't automatically move that value to the heap: the compiler's escape analysis decides whether it can remain on the stack. Benchmark the code rather than guessing from pointer syntax.
One of the reasons Go programs tend to use less memory than Java and C# programs is that Go tends to allocate more on the stack.
Is This Advice Accurate? How Can We Know?
I wanted to see for myself, so I wrote this benchmark that you can try if you're curious:
package main
import (
"fmt"
"testing"
)
type data struct {
a, b, c, d, e, f, g, h, i, j int64
}
var globalPtr *data
var globalValue data
func newDataPtr(i int) *data {
data := &data{int64(i), int64(i + 1), int64(i + 2), int64(i + 3), int64(i + 4), int64(i + 5), int64(i + 6), int64(i + 7), int64(i + 8), int64(i + 9)}
return data
}
func newData(i int) data {
data := data{int64(i), int64(i + 1), int64(i + 2), int64(i + 3), int64(i + 4), int64(i + 5), int64(i + 6), int64(i + 7), int64(i + 8), int64(i + 9)}
return data
}
func BenchmarkProcessValue(b *testing.B) {
for i := 0; i < b.N; i++ {
globalValue = newData(i)
}
// use it to avoid compiler optimization
fmt.Println(globalValue.a)
}
func BenchmarkProcessPointer(b *testing.B) {
for i := 0; i < b.N; i++ {
globalPtr = newDataPtr(i)
}
// use it to avoid compiler optimization
fmt.Println(globalPtr.a)
}
Slap that in a bench_test.go file and run go test -bench=. -benchmem to see the results. This is what I got:
wagslane@MacBook-Pro-2 test % go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: github.com/bootdotdev/go-api-gate/test
BenchmarkProcessValue-12 273343356 4.236 ns/op 0 B/op 0 allocs/op
BenchmarkProcessPointer-12 61566219 17.72 ns/op 80 B/op 1 allocs/op
PASS
ok github.com/bootdotdev/go-api-gate/test 2.912s
As you can see, passing by value rather than reference (pointer) is indeed faster in this case, even though the value is being copied. That doesn't prove that values are always faster; it only describes this benchmark.
That said, I admit it took me about 20 minutes of trial and error to get this benchmark into the state that I wanted to make sure it was testing what I wanted to test. There were initial drafts that I thought were copying to the heap, but they weren't. That in and of itself is a good lesson: the Go compiler is pretty smart and will optimize things for you. Don't go crazy trying to use pointers or non-pointers to optimize your code until you have something tangible to benchmark and optimize!
Frequently Asked Questions
When should I use a pointer in Go?
Use a pointer when you need a shared reference to a value, when a function or method must mutate the original value, when nil carries useful meaning, or when a benchmark shows that avoiding a large copy helps. Otherwise, use a value.
Is Go pass-by-value or pass-by-reference?
Go passes arguments by value. When you pass a pointer, the function receives a copy of the pointer, which still contains the address of the original variable.
What do * and & mean in Go?
The * syntax defines a pointer type and dereferences a pointer to get the original value. The & operator generates a pointer to its operand.
What happens when Go dereferences a nil pointer?
Dereferencing a nil pointer causes a runtime panic that crashes the program. If nil is a valid input, check for it before dereferencing the pointer.
Are pointers faster than values in Go?
Not always. A pointer can avoid copying a large value, but pointer syntax alone does not determine stack or heap allocation. Benchmark the code before choosing pointers for performance.
