GuidesHow to use pointers in Go

How to use pointers in Go

The power of C/C++ banks on the use of pointers. There are mixed views of its utility. Some opine it as an excellent feature while others deem it as something that is prone to misuse. Java, although it carries many of the legacy of C++ features, got rid of pointers explicit use except with JNI. However, the type of programming which Java is involved in does not lack luster without it. Go, meanwhile, retains some legacy of this C/C++ feature but not all. Pointers can do many interesting things that otherwise may not be possible. In this article we will learn some of the basics of pointer use in Go.

What is a pointer in Go?

In the following section, we will answer the question: what is a pointer in Go and the Golang programming language?

A simple variable typically holds values such as intVar :=10 or strVar := “Hello”. But there are also variables that refer to functions, channels, methods, maps, slices, and so forth. The latter use a type of variable called references. A pointer is also a variable but it holds the memory address of another variable. It is created to point to a particular type of variable. This means that the address of an integer variable type can only be stored in a pointer variable declared of the same type. This ensures that the Go compiler knows the size of bytes of the pointer to the value it occupies. As the address of the variable is stored in a pointer, we can modify the content of a variable through it.

Read: Working with Strings in Go.

What is the point of a pointer in Go?

One particular need for pointer variables is that they are cheap. This means they occupy a fixed size regardless of the size of the value they point to. The size of a pointer variable is 8-bytes for 64-bit machines and 4-bytes for 32-bit machines. Imagine a string variable which can be of any size but the pointer that points to it is of fixed size. This is important because we typically pass data to functions as arguments. If the function needs a local copy of the data, a normal variable would do. But if we want to alter the value of the original variable then instead of passing a variable by its value we may pass by reference through a pointer type variable. This ensures that even if we are in different function scope we are actually pointing to the original data.

Another aspect is that a variable passed by value actually creates a copy of itself. This occupies memory doubly. With pointers it actually references the original variable and does not create a copy and hence has less memory footprint.

Interestingly, in Go we can increase the lifetime of a variable independent of its scope using pointers. How? The internal memory management of Go will not claim the memory as long as there is at least one pointer pointing to that variable. (C/C++ do not have an internal memory manager, hence it is the programmer’s responsibility to do all the clean-ups.)

Examples of Using pointers in Go

The ampersand (&) operator in Go is taken or overloaded not only for binary operation as a bitwise AND operator but also as a unary operator to return the memory address of its operand. Therefore the role this operator plays depends upon the context of its use. An ampersand(&) put in front of a variable means we want to get the address of the variable.

There is another operator: asterisk(*). This is also overloaded as a multiplication binary operator and also as a pointer dereferencing unary operator. This is used to declare a pointer and also used to dereference to the content of the variable that it points to. Let us illustrate the ideas so far with a quick example. Here is an example of how to use pointers in Go:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	var a64 int64 // an int64 variable
	var pa64 *int64 = &a64 // address of int64 variable in a pointer of the same type 

	a64 = 1234567890 //dummy value

	fmt.Println("1. a64 contains ", a64) 
	fmt.Println("2. a64 address is ", &a64) 
	fmt.Println("3. pa64 holds the address of a64, which is ", pa64, " (same as 2)")
	fmt.Println("4. Accessing the content of a64 through pa64 ", *pa64, "(same as 1)")
	fmt.Println("5. Address of pa64 ", &pa64)

	var a16 int16
	var pa16 *int16

	a16 = 1256
	pa16 = &a16

	//compiler error: following is not allowed
	//pa64 = &a16 //cannot use &a16 (value of type *int16) as *int64 value in assignment
	//pa16 = &a64 // cannot use &a64 (value of type *int64) as *int16 value in assignment

	fmt.Println(a16)
	fmt.Println(pa16)

	fmt.Println("Pointer pa16 size: ", reflect.TypeOf(pa16).Size())
	fmt.Println("Pointer pa64 size: ", reflect.TypeOf(pa64).Size())
}

Entering this code and running it in your integrated development environment (IDE) or code editor results in the following output:

1. a64 contains  1234567890
2. a64 address is  0xc00010c000
3. pa64 holds the address of a64, which is  0xc00010c000  (same as 2)
4. Accessing the content of a64 through pa64  1234567890 (same as 1)
5. Address of pa64  0xc000102018
1256
0xc00010c028
Pointer pa16 size:  8
Pointer pa64 size:  8

Observe in the code above how we can access the content of the variable using asterisk(*) as the dereference operator. We can modify the content as follows:

a64 = 10
fmt.Println("a64 = ", a64)
*pa64++
fmt.Println("modified a64 = ", a64)

This results in the following output:

a64 =  10
modified a64 =  11

Note: An uninitialized pointer stores nil value.

var pi *int
if pi == nil {
	fmt.Println("uninitialized pointer")
}

A pointer, however, does not have to point to the same variable and can be set to point to a different variable provided the variable is of the same type.

How to Use Multilevel pointers in Golang

It is also possible to have pointers to pointers (double pointers) and the indirection can go on pointers to pointers to pointers…(triple or more). Note that we rarely need such multilevel pointers and things can turn messy with them. A practical advice is to think twice before declaring double or more pointers. Go maintains the legacy of C/C++ and has the provision. (Another reason Go often does not need multi level pointers is due to reference type variables). A quick example on how to use double pointers:

package main

import "fmt"

func main() {

	x := 10 //dummy value
	px := &x //assigned address of a variable
	ppx := &px //assigned address of another pointer

	fmt.Println("x =", x, ",px =", *px, ",ppx =", **ppx)
	*px = *px * 2
	fmt.Println("x =", x, ",px =", *px, ",ppx =", **ppx)
	**ppx = **ppx + 5
	fmt.Println("x =", x, ",px =", *px, ",ppx =", **ppx)
}

Running this creates the following output:

x = 10 ,px = 10 ,ppx = 10
x = 20 ,px = 20 ,ppx = 20
x = 25 ,px = 25 ,ppx = 25

Pointers as Function Receivers

When a function has a pointer type argument we can send the address of the variable. Any changes made to the variable in the function actually reflects the changes to the original variable. A classic example is the swap function, given below.

package main

import "fmt"

func localSwap(this, that string) {
	hold := this
	this = that
	that = hold
}

func realSwap(this, that *string) {
	hold := *this
	*this = *that
	*that = hold
}

func main() {

	this := "this"
	that := "that"

	fmt.Println("this = ", this, "that = ", that)
	localSwap(this, that)
	fmt.Println("After swap: this = ", this, "that = ", that) //no change
	realSwap(&this, &that)
	fmt.Println("After real swap: this = ", this, "that = ", that)
}

Running this in the code editor gives us the following result:

this =  this that =  that
After swap: this =  this that =  that
After real swap: this =  that that =  this

Observe that, as we invoke the localSwap function, the variables are passed by value. This simply means that a copy of the value is passed to the function. As a result, any changes made to them locally in the localSwap function does not really reflect any change in the original value. In the latter case, as we invoke the realSwap function, the variables are not passed by value but by reference. We send the memory address of the variable and these memory addresses are caught by the pointer variable function argument of the realSwap function. Therefore any changes made with the dereference operator on the pointer, the original variable gets modified. This is the reason why the localSwap function is unable to make any changes although the realSwap does with the same logic.

Read: Methods in Go Explained.

Go and Golang Tutorials

The use of pointers in Go is safer in comparison to C/C++ due to automatic memory management. Here, clearing memory after we are done with a pointer is not an issue, but with C/C++ it is, and must be taken care of explicitly. In Go, as long as at least one pointer refers to a variable, the memory for it is not reclaimed. This is also the reason why it is safe to return pointers to local variables which may have been created inside a function in Go. But this is particularly a bad idea in C/C++ except static variables.

Read more Go and Golang programming tutorials for developers.

Latest Posts

Related Stories