Golang Project Structure

Tips and tricks for writing and structuring Go code

Using Pointers in Go Code

Language

  • unknown

by

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.

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.

A boy holding his hand up in the air using his finger as a pointer.
Just as our fingers can point towards objects or people in the real world, a pointer variable “points” to a specific location in a computer’s internal memory.

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".

Leave a Reply

Your email address will not be published. Required fields are marked *