Understanding pointers in Go

2021.04.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Intro

In many programming languages like Go understanding pointers is a fundamental. To those unfamiliar with pointers, they only sound scary and are in fact fairly easy to grasp. Today we will go through the basic syntax, differences in passing arguments to functions by value and by reference and nil pointers. Let's begin.

Basic syntax

There are 2 elements we will use to visualize the pointers - & and * (ampersand and asterisk). Ampersand (&) is used for obtaining the pointer of a variable while asterisks for dereferencing a pointer and defining type of a variable.

func main() {
	var language string = "golang"
	var pointer *string = &language

	fmt.Println("language =", language)
	fmt.Println("pointer =", pointer)
	fmt.Println("*pointer =", *pointer)
}

We defined the language variable as a string and assigned it "golang" value.

Right below we define pointer variable as a pointer to a string and set its value to the pointer of our language variable using ampersand.

The output of the program is as below. Normally we would not print the pointer but for science we do it to show its value. As expected language is equal to string value and pointer is a address in memory. The last line is where we use asterisks (*) to dereference the pointer and in result we receive the golang string.

language = golang
pointer = 0xc000010200
*pointer = golang

Dereferencing a pointer not only gives us a value of a variable but allows to modify it.

func main() {
	var language string = "golang"
	var pointer *string = &language

	fmt.Println("language =", language)
	fmt.Println("pointer =", pointer)
	*pointer = *pointer + " is cool"
	fmt.Println("*pointer =", *pointer)
	fmt.Println("language =", language)
}
language = golang
pointer = 0xc000010200
*pointer = golang is cool
language = golang is cool

By dereferencing a pointer we both set the value and retrieved the value of the language variable.

Function Pointer Receiver

In Go you can define arguments of a function to be passed by value or by reference. When you pass argument by value, it means that a copy of the value is passed to the function, therefore any changes done to the value inside function will NOT be reflected outside the function. On the other hand passing by reference, in other words passing a pointer as the argument, allows you to make changes to the value outside the function.

When do we pass by reference and when by value? It all boils down to whether we want the value to be changed or not. For example when making a deposit in bank account, we want the value to change, we pass by reference in that case. In scenario where we want to see how much money will be left in the account after we withdraw certain amount of money, we don't want the balance to change, therefore we pass by value, substract the amount from the copy of the argument and return the result without touching the actual balance.

package main

import (
	"fmt"
)

type BankAccount struct {
	balance int
}

func Withdraw (account *BankAccount, amount int) error {
	fmt.Println("\nWithdrawing ", amount)
	if account.balance < amount {
		return fmt.Errorf("Not enough money in the account")
	}
	account.balance = account.balance - amount
	return nil
}

func BalanceAfterWithdraw (account BankAccount, amount int) int {
	fmt.Println("\nChecking balance after withdraw ", amount)
	account.balance = account.balance - amount
	return account.balance
}

func main() {
	account := BankAccount{
		balance: 100,
	}
	fmt.Println("account =", account)
	err := Withdraw(&account, 20)
	if err != nil {
		fmt.Println("error =", err)
	}
	fmt.Println("account =", account)
	balance := BalanceAfterWithdraw(account, 20)
	fmt.Println("predicted balance =", balance)
	fmt.Println("account =", account)
}

For the purpose of the article we define BankAccount structure with balance as integer. We also created two functions: Withdraw which takes pointer *BankAccount and amount as arguments and BalanceAfterWithdraw that takes BankAccount - not pointer and amount as its arguments. Notice the difference in syntax between pointer and BankAccount, first one is prefixed with asterisk (*).

account = {100}

Withdrawing  20
account = {80}

Checking balance after withdraw  20
predicted balance = 60
account = {80}

Calling the Withdraw function modifies the value outside the function because it takes pointer as its first argument. We can see the balance of the account was reduced by 20 and is equal to 80 after the call. Contrary to Withdraw function the BalanceAfterWithdraw takes the copy of the structure, so any changed made to the structure inside the functions are not reflected outside of it, and that's exactly what we can see in the output, the account remaining unchanged.

Nil pointer

In Go there is a zero value concept, that is all variables of specific type that have not been assigned a concrete value, default to the zero value. In case of the string type it is empty string (""), integer - zero (0), pointer - nil. Let us see what happens if we pass a nil pointer a the argument to our Withdraw function.

func main() {
	var account *BankAccount
	fmt.Println("account =", account)
	err := Withdraw(account, 20)
	if err != nil {
		fmt.Println("error =", err)
	}
	fmt.Println("account =", account)
}

We define the account variable to be a pointer type but we never initialize it, therefore its value is nil. The output is following.

account = <nil>

Withdrawing  20
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49ab6a]

goroutine 1 [running]:
main.Withdraw(0x0, 0x14, 0xc000070f58, 0x2)
        /home/michal/articles/pointers/function_rec.go:13 +0xaa
main.main()
        /home/michal/articles/pointers/function_rec.go:29 +0xc5
exit status 2

Because the argument passed to the function is nil, the program panics. To avoid that we should check the value before performing any operation on it.

func Withdraw (account *BankAccount, amount int) error {
	if account == nil {
		return fmt.Errorf("account argument is a nil pointer")
	}

	fmt.Println("\nWithdrawing ", amount)
	if account.balance < amount {
		return fmt.Errorf("Not enough money in the account")
	}
	account.balance = account.balance - amount
	return nil
}
account = <nil>
error = account argument is a nil pointer
account = <nil>

Conclusion

Our discovery of pointers concludes following points:

  1. Basic syntax, the difference between asterisk (*) and ampersand (&)

  2. Function receivers, when to pass by value or by reference and the consequences

  3. Nil pointers and how to handle them