Understanding Go pointers
Pointers are another essential tool for programming languages for efficient memory use. Some readers may have not encountered pointers in their current language, instead having used its cousin, the reference type. In Python, for example, the dict
, list
, and object
types are reference types.
In this section, we will cover what pointers are, how to declare them, and how to use them.
Memory addresses
In an earlier chapter, we talked about variables for storing data of some type. For example, if we want to create a variable called x
that stores an int
type with a value of 23
, we can write var x int = 23
.
Under the hood, the memory allocator allocates us space to store the value. The space is referenced by a unique memory address that looks like 0xc000122020
. This is similar to how a home address is used; it is the reference to where the data lives.
We can see the memory address where a variable is stored by prepending &
to a variable name:
fmt.Println(&x)
This would print 0xc000122020
, the memory address of where x
is stored.
This leads to an important concept: functions always make a copy of the arguments passed.
Function arguments are copies
When we call a function and pass a variable as a function argument, inside the function you get a copy of that variable. This is important because when you change the variable, you are only affecting the copy inside the function.
func changeValue(word string) { word += "world" }
In this code, word
is a copy of the value that was passed. word
will stop existing at the end of this function call.
func main() { say := "hello" changeValue(say) fmt.Println(say) }
This prints "hello"
. Passing the string and changing it in the function doesn't work, because inside the function we are working with a copy. Think of every function call as making a copy of the variable with a copy machine. Editing the copy that came out of the copy machine does not affect the original.
Pointers to the rescue
Pointers in Go are types that store the address of a value, not the value. So, instead of storing 23
, it would store 0xc000122020
, which is where in memory 23
is stored.
A pointer type can be declared by prepending the type name with *
. If we want to create an intPtr
variable that stores a pointer to int
, we can do the following:
var intPtr *int
You cannot store int
in intPtr
; you can only store the address of int
. To get the address of an existing int
, you can use the &
symbol on a variable representing int
.
Let's assign intPtr
the address of our x
variable from previously:
intPtr = &x intPtr now stores 0xc000122020.
Now for the big question, how is this useful? This lets us refer to a value in memory and change that value. We do that through what is called dereferencing the pointer. This is done with the *
operator on the variable.
We can view or change the value held at x
by dereferencing the pointer. The following is an example:
fmt.Println(x) // Will print 23 fmt.Println(*intPtr) // Will print 23, the value at x *intPtr = 80 // Changes the value at x to 80 fmt.Println(x) // Will print 80
This also works across functions. Let's alter changeValue()
to work with pointers:
func changeValue(word *string) { // Add "world" to the string pointed to by 'word' *word += "world" } func main() { say := "hello" changeValue(&say) // Pass a pointer fmt.Println(say) // Prints "helloworld" }
Note that operators such as *
are called overloaded operators. Their meaning depends on the context in which they are used. When declaring a variable, *
indicates a pointer type, var intPtr *int
. When used on a variable, *
means dereference, fmt.Println(*intPtr)
. When used between two numbers, it means multiply, y := 10 * 2
. It takes time to remember what a symbol means when used in certain contexts.
But, didn't you say every argument is a copy?!
I did indeed. When you pass a pointer to a function, a copy of the pointer is made, but the copy still holds the same memory address. Therefore, it still refers to the same piece of memory. It is a lot like making a copy of a treasure map on the copy machine; the copy still points to the place in the world where you will find the treasure. Some of you are probably thinking, But maps and slices can have their values changed, what gives?
They are a special type called a pointer-wrapped type. A pointer-wrapped type hides internal pointers.
Don't go crazy with pointers
While in our examples we used pointers for basic types, typically pointers are used on long-lived objects or for storage of large data that is expensive to copy. Go's memory model uses the stack/heap model. Stack memory is created for exclusive use by a function/method call. Allocation on the stack is significantly faster than on the heap.
Heap allocation occurs in Go when a reference or pointer cannot be determined to live exclusively within a function's call stack. This is determined by the compiler doing escape analysis.
Generally, it is much cheaper to pass copies into a function via an argument and another copy in the return value than it is to use a pointer. Finally, be careful with the number of pointers. Unlike C, it is uncommon in Go to see pointers to pointers, such as **someType
, and, in over 10 years of coding Go, I have only once seen a single use for ***someType
that was valid. Unlike in the movie Inception, there is no reason to go deeper.
To sum up this section, you have gained an understanding of pointers, how to declare them, how to use them in your code, and where you should probably use them. You will use them on long-lived objects or types holding large amounts of data where copies are expensive. Next, let's explore structs.