Using Pointers in Go Code
Language
- unknown
by James Smith (Golang Project Structure Admin)
It’s possible to write thousands of lines of Go code without declaring a pointer even once. However, pointers are an important part of the programming language, so it’s important to ensure that you understand how they work.
In this post we will discuss the definition of a pointer and then go on to look at some practical code examples of pointers being declared and used.
Table of Contents
What Is a Pointer?
If you’ve ever used a language like C or C++, you will probably already have an understanding of what pointers are.
Go uses pointers too, but they don’t play as integral a role in most Go code as they do in C-based languages — the Go compiler also attempts to abstract away some of the complexity involved in using them.
To put it simply, a pointer is a variable that is used to store the memory address of another variable.
All data used in programming languages like Go is stored at specific memory locations in RAM, known as addresses, but since typing out these locations would be arduous, we use variables that have more human-friendly names in order to refer to each item of data instead.
Pointers are typed, just like the variables that they point to, so an int64
pointer can only store a memory address that holds a 64-bit integer.
It is possible to use the pointer either to read or to modify the value that is stored at the memory address held in the pointer.
When data is accessed using the memory address stored in a pointer, that pointer is said to have been dereferenced.
How to See the Memory Location Held by a Pointer
Let’s look now at an example of declaring and using a pointer in Go:
package main
import "fmt"
func main() {
number := 2056
pointerToNumber := &number
fmt.Printf("%x\n", pointerToNumber)
}
Placing an ampersand before the name of a variable will take the memory address of that variable and create a pointer to hold it, as you can see in the declaration of pointerToNumber
above.
When we use the "%x"
format verb in the fmt.Printf
function, we are requesting that the value stored in pointerToNumber
be printed as a hexadecimal number ("%d"
would have printed the variable as a decimal number).
This will not print the value stored at the pointer’s memory address, but it will instead print the number representing the memory address itself.
When I ran the code above on the Go Playground, I got the following output:
c00001c030
When converted to decimal (824633835568
), this a number in the order of hundreds of millions, which is indicative of just how many different locations in memory a modern machine is able to access.
The largest number of memory addresses that a 64-bit processor can theoretically access is equivalent to two to the power of sixty-four, which would equate to many thousands of petabytes of memory.
Of course, no practical machine has that much memory, but it’s nice to know that our processors are ready to handle future expansion.
Modifying Memory Through a Pointer
The example below shows how to change the value held in the memory address stored in a pointer:
package main
import "fmt"
func main() {
number := 2056
pointerToNumber := &number
fmt.Printf("%d\n", *pointerToNumber)
*pointerToNumber++
fmt.Printf("%d\n", *pointerToNumber)
}
The asterisk immediately before the name of a pointer variable is known as the dereferencing operator. It is used to access the value at the memory address held in the pointer.
We use the dereferencing operator with the fmt.Printf
function in order to output the value stored in the number
variable (which the pointerToNumber
variable points at).
We then use the dereferencing operator in combination with the increment operator in order to add one to the previous value.
When we finally print out the value referenced by pointerToNumber
again, we will see that it is now 2057
, which, as expected, is greater by one than its previous value of 2056
.
Declaring a New Pointer
We showed above how to declare a pointer to an existing variable, however, it is also possible to create a pointer that points to a portion of newly allocated memory, rather than corresponding to an existing variable.
We can use the new
keyword in order to allocate enough memory to hold a value of the specified type and to obtain a pointer that will allow us to access it.
Let’s look at the example below:
package main
import "fmt"
func main() {
pointerToNumber := new(int)
fmt.Println(*pointerToNumber)
}
On a 64-bit machine, calling new(int)
will allocate 8 bytes of memory, enough to hold a 64-bit integer. On a 32-bit machine, it would allocate 4 bytes of memory. In either case, there will be enough memory to hold a standard integer.
Notice how the value stored at the pointer’s memory location has been initialized to the type’s zero value by the Go compiler, so when we print out the number, it is 0
, even though we hadn’t assigned that value ourselves.
Using Pointers to Mutate Data In a Struct
Look at the following example:
package main
import "fmt"
type User struct {
name string
}
func (user User) setName(newName string) {
user.name = newName
}
func main() {
user := User{
name: "John",
}
user.setName("Jack")
fmt.Println(user.name)
}
We have defined a struct in order to hold some information about the users of our software.
In the main
function, we initialize a User
variable, setting the name to "John"
. However, we have also created a setName
method, which is intended to allow us to change the name stored in our struct. We call this method with the string "Jack"
, in order to change our user’s name to that value.
When we finally print out the name, will it be "John"
or "Jack"
?
When we run the code above, the name will remain as "John"
, because our setName
method didn’t work as we had intended. When we call the method, a new User
struct will be instantiated, with the information from our existing user
copied over. So we change the name correctly within this cloned struct, but when the method ends, it is lost.
If we wanted the change of name to persist, we would have to use a pointer receiver in the method declaration.
However, the setName
method below would be able to access the original struct via a pointer, thereby overwriting any changes to its data:
func (user *User) setName(newName string) {
user.name = newName
}
If you modify the original example to use the updated method, you will see that the name will correctly change to "Jack"
.