You Might Not Need a Pointer

I spent like an hour last night explaining this in a group chat and I need to justify my husband’s suffering, so I’m going to share my rules of thumb when it comes to pointers in Go.

When to use pointers

There are four things pointers are useful for:

  1. Accepting nil values.
  2. Mutating data across functions.
  3. Avoiding copying data.
  4. Fulfilling an interface.

Accepting nil values.

A fairly straightforward use case. If you need to be able to distinguish between the zero value ("", 0, false, whatever) and not there at all, a pointer is a good way to do that. A nil value is the absence of data, a pointer to the zero value is the zero value. If a boolean is optional and you want to know if it was specified as false or just not specified, a pointer to a boolean helps you make that distinction.

Mutating data across functions.

Go is a pass-by-value language, meaning when you pass an argument to a function, the runtime copies that argument to a whole new chunk of memory and gives the copy to the function. So if you change that parameter’s contents inside the function the caller won’t see those changes, unless the parameter is itself a reference type, like a pointer. Because the data a pointer holds is a location in memory, copying that pointer to pass to the function means it still points to the same location in memory, there are just now two pointers pointing to that location in memory.

So if you want to be able to modify data in place, a pointer is a great way to do that.

Avoiding copying data.

Because Go is a pass-by-value language, every time you pass a variable to a function the variable gets copied to a new location in memory. Writing to memory is fast, but not free, and for some very performance-sensitive things it can be considered “slow”. And everything you write to memory the garbage collector eventually needs to clean up, and the more garbage there is the more resources it’s using and the more resources cleaning up after everything takes.

So if you have a really, really big variable or are passing it to an exorbitant number of functions, you can save some resources by passing a pointer. A pointer is a fixed width, usually 4 or 8 bytes (depending on the system the program is running on), and usually much smaller than the data it’s describing. That means less time copying data and less garbage to clean up.

If you’re using pointers for this benefit, you should have a really compelling case that the copying or the garbage collection are actively causing you problems. Most things will never even notice the difference this brings.

Fulfilling an interface.

If a type is filling an interface, and any of the methods implementing that interface use a pointer receiver, you can only use a pointer as that interface type. Trying to use a non-pointer will throw a compiler error. It’s often best to just be consistent within the methods implementing an interface whether you’re using a pointer or not as the receiver. And you should only use a pointer as the receiver for any of them if you need to for one of the other three reasons.

When not to use pointers

I’ve seen a lot of code (professionally, in the wild, that I’ve written, whatever) where a pointer is being used, either as a property on a struct or as the return type from a function, for reasons I can only describe as “vibes”. The nil value has no semantic meaning that ever gets used. The value is never mutated. The amount of data being conveyed is barely bigger than the pointer itself.

The most common thing I hear about using a pointer as a function return is “so I can return nil, err when there’s an error”. But you don’t need to do that. The error is already conveying the meaning that something happened and the other return value is garbage. Just return myType{}, err, it’s fine. And callers won’t need to check if the return value is nil, or wonder if their modifications are going to have impacts somewhere else.

For structs with pointers as properties, I think it’s usually an attempt to avoid copying too much data around. I think people internalized that advice really prematurely, and just default to pointers when using another struct type as a property in a struct.

Don’t do that. Default to non-pointer types, and only reach for the pointers when you provably need one of the four benefits of pointers. Trying to figure out if a struct property is ever going to be nil and where it was set is a nightmare if the struct property is a pointer. Embrace pointers as communicating an absence of guarantees around a bit of data, and make as many guarantees as you can. Your code will be easier to read, navigate, and extend, and when you see a pointer you’ll know it means something.