Pointers in Go

A pointer is a variable that stores the memory address data referenced by another variable. Pointers have the power to mutate data they are pointing to.

Before we begin talking about pointers, let’s learn a thing or two about hexadecimal numbers. A hexadecimal number is a number with base 16. If you are a web developer, then you are using them for a long time, because mostly; colors are represented in hex format. For example, white is represented as #FFFFFF and black as #000000.

In Go, you can save a hexadecimal number in a variable and Go provides a literal expression for that. If a number starts with 0x then it’s a hexadecimal number.

package main

import "fmt"

func main() {
	a := 0x00
	b := 0x0A
	c := 0xFF

	fmt.Printf("variable a of type %T with value %v in hex is %X\n", a, a, a)
	fmt.Printf("variable b of type %T with value %v in hex is %X\n", b, b, b)
	fmt.Printf("variable c of type %T with value %v in hex is %X\n", c, c, c)
}

// variable a of type int with value 0 in hex is 0
// variable b of type int with value 10 in hex is A
// variable c of type int with value 255 in hex is FF

From the above example, we can see that values represented in the hexadecimal system are saved in the decimal system with data type int.

But why we are learning about hexadecimal numbers when we are talking about pointers. Well, let’s talk about the memory address first.

When you are declaring a variable and providing some value (data), the Go runtime will allocate some memory for the value in the RAM and depending on data type, it will allocate a specific size of memory to store that value.

That memory will have some memory address (like a postal address) so that Go can find that variable’s value when asked for it. These memory addresses are represented in hexadecimal values.

How to access the memory address of a variable?

To access the address value (data) represented by a variable, Go provides the & (ampersand) operator which is used in front of the variable name. By doing this, &variable_name expression returns the memory address of the value (data) referenced by variable_name variable.

package main

import "fmt"

func main() {
	a := 0x00
	b := 0x0A
	c := 0xFF

	fmt.Println("&a =", &a)
	fmt.Println("&b =", &b)
	fmt.Println("&c =", &c)
}

// &a = 0xc00012a010
// &b = 0xc00012a018
// &c = 0xc00012a020

We saw this in slice lesson when we were trying to prove that two slices can reference values from the same array. In the above example, using & operator, we found the memory address of the variable a, b and c.

What is a pointer?

A pointer is a variable that points to the memory location of another variable (actually to the value referenced by the variable).

As we saw earlier, we can save a hexadecimal value to a variable, but under the hood, it is saved as a decimal value of type int. But there is a catch.

Even though you could save a memory address (hexadecimal number) in a variable, it’s not a pointer or it does not point to the memory location of another variable. It is just a value and it has no idea what that value means.

That’s where pointer comes into play. A pointer is just a variable but of special kind and special data type.

A pointer also saves the memory address but it knows where that memory is located in the RAM and how to retrieve the value stored in that memory address. It can perform various kinds of operations with it like it can read the value stored at the memory address or write a new value.

Unlike saving hex value in a variable which has type int, the pointer has the data type of *int if it points to the memory address of int data and *string if it points to the memory address of string data.

Syntax to create or define a pointer is var p *Type where Type is a data type the value (data) it will be pointing to. Let’s create a simple pointer.

package main

import "fmt"

func main() {
	var pa *int

	fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}

// pointer pa of type *int with value <nil>

In the above example, we have created pointer pa which points to the data of type int but since we are not assigning any initial value to it, its zero-value is nil. A pointer has a nil value because it is not pointing to any data (value) in the RAM at the moment.

So let’s create a variable of type int and make pa point to it.

package main

import "fmt"

func main() {
	a := 1
	var pa *int
	pa = &a

	fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}

// pointer pa of type *int with value 0xc000012028

In the above example, we have created a variable a and assigned an initial value of 1. Go will save an integer 1 somewhere in the RAM. Then we have created the pointer pa which can point to int value.

Later, we have assigned the memory address of the variable a (its value actually) to the pointer pa using pa = &a expression. The above program can also be written with a shorthand variable assignment format.

package main

import "fmt"

func main() {
	a := 1
	pa := &a

	fmt.Printf("pointer pa of type %T with value %v\n", pa, pa)
}

// pointer pa of type *int with value 0xc0000aa010

In the shorthand format, Go will interpret that we are trying to create a pointer because we are assigning the memory address of a variable (using the & operator) to the variable we are trying to create.

When print the value of pa, it returns the memory address it is pointing. Also, the data type of pa is *int which means that it is a pointer that points to the data of type int.

However, it does not explicitly mention which variable or data it points to. But it can find out the data in that memory address.

Dereferencing a Pointer

To find out the value (data) a pointer it points to, we need to use * operator, also called dereferencing operator which if placed before a pointer variable (like & operator to get memory address), it returns the data in that memory.

package main

import "fmt"

func main() {
	a := 1
	pa := &a

	fmt.Printf("data at %v is %v\n", pa, *pa)
}

// data at 0xc000012028 is 1

Changing the variable value using a Pointer

As we saw in the previous example, we can read the data at the memory location a pointer points to, but we can also change (write) the value at that memory location.

package main

import "fmt"

func main() {
	a := 1
	pa := &a
	*pa = 2

	fmt.Printf("a = %v\n", a)
	fmt.Printf("data at %v is %v\n", pa, *pa)
}

// a = 2
// data at 0xc0000aa010 is 2

As you can see from the above example, *pa syntax reads the value from the memory location pointed by the pointer, but the same expression can be used to write a new value at the same memory location.

If you are wondering, why the value of the variable a changed. This is because a new value has been written at the memory address referenced by the variable a. This proves that pointers are pretty powerful.

💡 The difference between a variable and pointer is that a variable stores the value at a memory address and pointer points to a memory address.

The new function

Go provides the new built-in function which allocates memory and returns a pointer to that memory. The syntax of new function is as follows.

func new(Type) *Type

The first argument of the new function is the data type and the returned value of this function is the pointer of that data type. This function will allocate some memory, write a zero-value of the Type at that memory location and return a pointer to that memory location.

package main

import "fmt"

func main() {
	pa := new(int)

	fmt.Printf("data at %v is %v\n", pa, *pa)
}

// data at 0xc000124010 is 0

Did you expect the value (data) at memory location returned by the new function to be nil? Well, the zero-value of a pointer is nil which means pointer is not pointing to any memory but when the pointer points it to a memory location, memory cannot be empty, it must hold some data.

Go stores zero-value of data type passed to the new function and returns the memory address of it. Therefore, if you are interested in a pointer only, then you can use the new function instead of creating a new variable and then a pointer which points to the value of the variable (as we saw earlier).

💡 Hence the definition “A pointer is a variable that points the memory address of another variable” is strictly not true. “A pointer is a variable which points a memory address” is more accurate.

Passing a Pointer to a Function

Like a variable, you can pass a pointer to a function. There are two ways you can do this. Either create a pointer and then pass it to the function or just pass an address of a variable.

package main

import "fmt"

func changeValue(p *int) {
	*p = 2
}

func main() {
	a := 1
	pa := &a
	changeValue(pa)

	fmt.Printf("a = %v\n", a)
}

// a = 2

In the above program, we passed the pointer pa as an argument to the changeValue function. This pointer points to the value of the variable a. Hence inside the function, we can write a new value at the memory address pointed by the pointer pa which also mutates the value of the variable a.

Instead of taking this long approach, we can shorten the above example by passing the address of the variable a as an argument instead. The changeValue function parameter p is now the pointer.

💡 Two pointers pointing to the same value are the same.

package main

import "fmt"

func changeValue(p *int) {
	*p = 2
}

func main() {
	a := 1
	changeValue(&a)

	fmt.Printf("a = %v\n", a)
}

// a = 2

In the above program, the argument syntax of the changeValue function instructs Go that we are expecting a pointer, especially the *int part (type declaration) of the argument p.

You can pass a pointer of a composite data type like array to function.

package main

import "fmt"

func changeValue(p *[3]int) {
	//*p == original array `a`
	// *p[0] != (*p)[0]
	(*p)[0] *= 2
	(*p)[1] *= 3
	(*p)[2] *= 4
}

func main() {
	a := [3]int{1, 2, 3}
	changeValue(&a)

	fmt.Printf("a = %v\n", a)
}

// a = [2 6 12]

We could also write the above program using shorthand syntax provided by Go to access data from an array pointer.

package main

import "fmt"

func changeValue(p *[3]int) {
	// (*p)[0] == p[0]
	p[0] *= 2
	p[1] *= 3
	p[2] *= 4
}

func main() {
	a := [3]int{1, 2, 3}
	changeValue(&a)

	fmt.Printf("a = %v\n", a)
}

// a = [2 6 12]

But passing array pointer as function parameter is not idiomatic to Go. We should prefer slices instead for this functionality. As we saw in the slices lesson, we can pass a slice as an argument to a function and that function can mutate the values inside the slice.

Pointer arithmetic

Unlike C language, where a pointer can be incremented or decremented, Go does not allow pointer arithmetic.

#golang #pointers