For people beginning with some programming languages, like Go, C, or C++, pointers can be a little confusing. It doesn’t help that the syntax used depends on context in order to understand what’s happening in the code. This post is purely about pointers in Go, but some of the understandings can be transferred to other languages.
To begin with, a pointer is a variable that stores a memory address. That’s all it is. The pointer is typed in Go, meaning that the value stored at the memory address must be of the stipulated type.
If you are more comfortable with analogies, then think about files on the internet. If I give you a variable of type string that contains the URL of a file, then (effectively) I have given you a pointer to that file.
Pointer Syntax
With Go a *
character is used to denote that you are talking about a pointer, eg:
// var declaration
var myPtr *int
// declared in a function
func foo(s *string){}
// a method on the pointer of a type
func (t *T) func foo(){}
Unfortunately for beginners the *
character is also used to dereference a value from a pointer.
// assign the value stored at the memory address that *s points to to x
x := *s
Dereferencing is the retrieval of the value that is stored at the memory address the pointer being dereferenced is pointing to (whew, that’s a mouthful!).
To use the internet file analogy, dereferencing is getting a copy of the file stored at the URL that the pointer holds.
If you now understand that the pointer is just holding an address to memory, then it follows that you can assign to a pointer type an address. This is true with one important exception.
In Go you cannot take the address of a constant. This is simply because if you have the address of a constant, you can change that value in memory, and that value can no longer be considered a constant. You can only create a pointer to a variable.
A &
character is used to denote that you are talking about the address, eg:
// will error
x := &int32(42)
// correct
y := int32(42)
x := &y
// dereference x and assign the value to z
z := *x
For the internet file analogy we are giving the URL to the file to our pointer.
Pointer usage
The syntax surrounding pointers should now be clear, the question that might now arise is, why should they be used.
First and foremost pointers are talking about a piece of memory, not the value contained in them, so when a pointer is being used it means (literally) “Use this piece of memory”. If I give you a copy of my value, you can do anything with it, I will never know what you did, and I don’t care, and, equally, you will never know if I do anything to my copy.
But when we need to be talking about the same value, including any changes that are made to it, then a pointer is appropriate.
A slightly more concrete example, you can look at it on Playground too:
package main
import (
"fmt"
)
func main() {
i := 5
j := 5
foo(i,&j)
fmt.Printf("I: %d, J: %d\n", i, j)
}
func foo(i int, j *int) {
i = i + 17
*j = *j + 17
}
The output will show that foo
mutated j
, but didn’t mutate i
. That is because i
in foo
is a copy of i
in main
but j
in foo
is a pointer to j
in main
so foo
altered the value stored in the memory address held in j
.
This is great for developers, there’s no need to communicate back and forth between functions the changes that have been made, but, and this is important, the code is no longer threadsafe.
Thread Safety
Pointers are not thread safe. That is, if more than one thread (or goroutine) access the pointer at the same time, and at least one of us writes to the address held by the pointer then the outcome is data race
. The value that the reader gets back from their read is determined by the outcome of the race between the two threads. Ditto for two writers, the value held at the address is determined by a race between the two writers (last writer wins, right).
The potential for a race condition
also exists.
So, whenever pointers are being used in concurrent environments then they must be guarded with synchronisation patterns (eg. mutex, channels, semaphores), like any piece of critical code.
I’m not going to go in detail about the synchronisation patterns here as I have already written a post on Go synchronisation patterns however, I will note that in Go an RWMutex would be most useful for this usage of pointers because it would allow multiple reader threads to access the pointer value concurrently, and only prevent new reads when a write
lock is taken.