We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

When to Use Pointers in Go

Lane Wagner
Lane WagnerBoot.dev co-founder and backend engineer

Last published

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:

  1. First, worry about writing clear, correct, maintainable code.
  2. 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:

  1. Stack vs. Heap
  2. 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.