Don't fear the pointer
From For the Love of Go
You will understand pointers in Go after reading this, or your money back
Go pointers aren’t as scary as they might sound, especially if you’re new to programming or don’t have a computer sciencey background. In fact, they’re so extremely straightforward that everyone I explain them to says “But that isn’t complicated at all!”
Exactly. And I hope you’ll be saying the same by the end of this piece, which aims to explain pointers in Go in simple terms: what they are, why we need them, and what to watch out for when using them.
Why do we need pointers?
Let’s create a new package so that we can play around with some ideas
and see how they work. Create a new folder, wherever you keep your Go
code, and name it pointerplay
.
Inside this folder, run go mod init pointerplay
, to tell
Go you’re creating a new module called pointerplay
, and
create a new empty file called pointerplay_test.go
.
We’re going to write a test to experiment with function calls and
values. Suppose we write a test for a function in the
pointerplay
package called Double
. It should
take one parameter, an integer, and multiply it by 2.
Here’s something that should do the job:
package pointerplay_test
import (
"pointerplay"
"testing"
)
func TestDouble(t *testing.T) {
.Parallel()
tvar x int = 12
:= 24
want .Double(x)
pointerplayif want != x {
.Errorf("want %d, got %d", want, x)
t}
}
Nothing fancy here: all Double
needs to do is multiply
its input by 2. Here’s my first attempt at an implementation, in the
pointerplay.go
file:
package pointerplay
func Double(input int) {
*= 2
input }
You may know that input *= 2
is a handy short form for
input = input * 2
. Its effect is to double the value of
input
.
So does the test pass now? Well, let’s run go test
and
see:
--- FAIL: TestDouble (0.00s)
pointerplay_test.go:14: want 24, got 12
Oh no!
The mystery of the failing test
What’s going on? Is the Double
function broken?
Actually, Double
is doing exactly what we asked it to
do. It receives a parameter we call input
, and it
multiplies that value by 2:
*= 2 input
So why is it that, in the test, when we set x
to 12, and
call Double(x)
, the value of x
remains 12?
Shouldn’t it now be 24?
var x int = 12
:= 24
want .Double(x) pointerplay
Parameters are passed by value
The answer to this puzzle lies in what happens when we pass a value
as a parameter to a Go function. It’s tempting to think, if we have some
variable x
, and we call Double(x)
, that the
input
parameter inside Double
is simply the
variable x
. But that’s not the case.
In fact, the value of input
will be the same as the
value of x
, but they’re two independent variables. The
Double
function can modify its local input
variable as much as it likes, but that will have no effect on the
x
variable back in the test function—as we’ve just
proved.
The technical name for this way of passing function parameters is
pass by value, because Double
receives only the
value of x
, not the original x
variable
itself. This explains why x
wasn’t modified in the
test.
Creating a pointer
Is there any way, then, to write a function that can modify a
variable we pass to it? For example, we’d like to write a version of
Double
that will actually have an effect on x
when we call Double(x)
. Instead of just taking a copy of
the value of x
at the moment of the function call, we want
to pass Double
some kind of reference to x
.
That way, Double
could modify the original x
directly.
Go lets us do exactly that. We can create what’s called a
pointer to x
, using this syntax:
.Double(&x) pointerplay
You can think of the &
(pronounced ‘ampersand’) here
as the sharing operator; it lets you share a variable with the
function you’re passing it to, so that the function can modify it.
Declaring pointer parameters
There’s still something missing, though, because our modified test doesn’t compile:
cannot use &x (type *int) as type int in argument to pointerplay.Double
The compiler is saying that Double
takes a parameter of
type int
, but what we tried to pass it was a value of type
*int
(pronounced “pointer to int”).
These are two distinct types, and we can’t mix them. If we want the
function to be able to take a *int
, we’ll need to update
its signature accordingly:
func Double(input *int) {
It’s worth adding that the type here is not just “pointer”, but
specifically “pointer to int”. For example, if we tried to pass a
*float64
here, that wouldn’t work. A *float64
and a *int
are both pointers, but since they’re pointers to
different types, they are also different from each other.
What can we do with pointers?
We’re still not quite done, because even with our updated function signature, the compiler isn’t happy with this line:
*= 2 input
It complains:
invalid operation: input *= 2 (mismatched types *int and int)
Another type mismatch. It’s saying “you tried to multiply two
different kinds of thing”. The numeric constant literal 2
is interpreted as an int
, while input
is a
pointer.
The *
operator
Instead of trying to do math with the pointer itself, we need the value that the pointer points to. Because a pointer is a reference to some variable, the fancy name for this is dereferencing the pointer.
To get the value pointed to by input
, we write
*input
(“star-input”):
*input *= 2
Nil pointers and panics
You know that every data type in Go has some default value. If you
declare a variable of type int
, for example, it
automatically has the value 0 unless you assign some other value to
it.
So what’s the default value of a pointer type? If you declare a
variable of type *int
, what value does it have?
The answer is the special value nil
, which you’ve
encountered many times already in connection with error
values. Just as a nil error value means “no error”, a nil pointer value
means “doesn’t point to anything”.
It makes no sense to dereference a nil pointer, then, and if this situation arises while your program is running, Go will stop execution and give you a message like:
panic: runtime error: invalid memory address or nil pointer dereference
This shouldn’t happen under normal circumstances, so “panic” in this context means something like “unrecoverable internal program error”.
Pointer methods
If functions can take pointers as parameters, then can the receiver of a method also be a pointer? Yes, it can. Such a method is called a pointer method, and it’s useful because you can write methods that modify the variable they’re called on.
Could we modify Double
, using our new knowledge about
pointers, to turn it into a method? Let’s find out. Here’s what the
relevant part of our test looks like right now:
.Double(&x) pointerplay
How do we turn this into a method call on &x
? We
might try something like this:
&x.Double()
But that doesn’t quite work:
x.Double undefined (type int has no field or method Double)
The compiler misunderstood what we wanted. What we wanted was to
create a pointer to x
, using the sharing operator, and then
to call the Double
method on that pointer. But what our
syntax actually said was to create a pointer to the value
returned by x.Double()
!
There’s some ambiguity here about which of the two operators should be applied first: the method call, or the sharing operator? We can clarify this using parentheses:
(&x).Double()
While this satisfies the compiler, it’s rather cumbersome, and we’d prefer not to smash together the two operations of creating a pointer and calling a method into the same statement. Instead, let’s create the pointer first, then call the method:
:= &x
p .Double() p
Much clearer. The compiler will now prompt us to complete the
refactoring by changing Double
from a function to a
method:
p.Double undefined (type *int has no field or method Double)
Creating custom types
How can we update the definition of Double
to make the
test pass?
A completely reasonable guess at this might be:
func (input *int) Double() {
But straight away we run into a problem:
cannot define new methods on non-local type int
We’re not allowed to add methods on a type we didn’t define. That’s
easy to deal with, though. We can define a new type
MyInt
:
type MyInt int
func (input *MyInt) Double() {
*input *= 2
}
This solves the method definition problem, but we also need to update
the test to use values of MyInt
rather than plain old
int
:
func TestDouble(t *testing.T) {
.Parallel()
t:= pointerplay.MyInt(12)
x := pointerplay.MyInt(24)
want := &x
p .Double()
pif want != x {
.Errorf("want %d, got %d", want, x)
t}
}
And we’re there!
PASS
Pointers vs values
Now that you understand when and why we use pointers in Go, you might still be wondering when to write a value method (one that takes a value) and when to write a pointer method (one that takes a pointer to a value).
There’s a very simple way to decide whether to use a value or a pointer receiver. Ask yourself: “Does this method need to modify the receiver?”
If the answer is yes, then it should take a pointer. You can see why,
can’t you? If it took a value, then it could modify that value as much
as it liked, but the change wouldn’t affect the original variable (like
our first version of the Double
function).
On the other hand, if the method doesn’t need to modify the receiver, it doesn’t need to take a pointer (and by taking a value, it signals that fact to anyone reading the code, which is useful).
If we’re writing some method that modifies its receiver, but we don’t take a pointer, then any changes to it we might make in the method don’t persist. This can lead to subtle bugs which are hard to detect without careful testing. (I write this bug about once a day, so if the same thing happens to you, don’t feel too bad.)
In fact, most IDEs with Go support will warn you about this problem:
ineffective assignment to variable
In my book The Deeper Love of Go, we’ll learn all about pointers and methods, putting them to work in a fun and involving project: an online bookstore. It’s a gentle introduction to everything you need to write useful Go programs, and understand how the language works. I hope you’ll take a look!