Kealan Parr Software engineer, technical writer and member of the Unicode Consortium.

How to use pointers in Go

5 min read 1540

Go Logo With a Sign Pointing in Two Directions in the Background

Go’s popularity has exploded in recent years. The 2020 HackerEarth Developer survey found that Go was the most sought-after programming language among experienced developers and students. The 2021 Stack Overflow Developer survey reported similar results, with Go being one of the top four languages that developers want to work with.

Given its popularity, it’s important for web developers to master Go, and perhaps one of the most critical components of Go is its pointers. This article will explain the different ways pointers can be created and the types of problems pointers fix.

What is Go?

Go is a statically typed, compiled language made by Google. There are many reasons why Go is such a popular choice for building robust, reliable, and efficient software. One of the biggest draws is Go’s simple and terse approach to writing software, which is apparent in the implementation of pointers in the language.

Passing arguments in Go

When writing software in any language, devs must consider what code could mutate in their codebase.

When you begin to compose functions and methods and pass around all different types of data structures in your code, you need to be careful of what should be passed by value and what should be passed by reference.

Passing an argument by value is like passing a printed copy of something. If the holder of the copy scribbles on it or destroys it, the original copy you have is unchanged.

Passing by reference is like sharing an original copy with someone. If they change something, you can see — and have to deal with — the changes they’ve made.

Let’s start with a really basic piece of code and see if you can spot why it might not be doing what we expect it to.

package main

import (
  "fmt"
)

func main() {
  number := 0
  add10(number)
  fmt.Println(number) // Logs 0
}

func add10(number int) {
  number = number + 10 
}

In the above example, I was trying to make the add10() function increment number 10, but it doesn’t seem to be working. It just returns 0. This is exactly the issue pointers solve.

Using pointers in Go

If we want to make the first code snippet work, we can make use of pointers.

We made a custom demo for .
No really. Click here to check it out.

In Go, every function argument is passed by value, meaning that the value is copied and passed, and by changing the argument value in the function body, nothing changes with the underlying variable.

The only exceptions to this rule are slices and maps. They can be passed by value and because they are reference types, any changes made to where they’re passed will change the underlying variable.

The way to pass arguments into functions that other languages consider “by reference” is by utilizing pointers.

Let’s fix our first example and explain what’s happening.

package main

import (
  "fmt"
)

func main() {
  number := 0
  add10(&number)
  fmt.Println(number) // 10! Aha! It worked!
}

func add10(number *int) {
  *number = *number + 10 
}

Addressing pointer syntax

The only major difference between the first code snippet and the second was the usage of * and &. These two operators perform operations known as dereferencing/indirection (*) and referencing/memory address retrieval (&).

Referencing and memory address retrieval using &

If you follow the code snippet from the main function onward, the first operator we changed was to use an ampersand & in front of the number argument we passed into the add10 function.

This gets the memory address of where we stored the variable in the CPU. If you add a log to the first code snippet, you will see a memory address represented with hexadecimal. It will look something like this: 0xc000018030 (it will change each time you log).

This slightly cryptic string essentially points to an address on the CPU where your variable is stored. This is how Go shares the variable reference, so changes can be seen by all the other places that have access to the pointer or memory address.

Dereferencing memory using *

If the only thing we have now is a memory address, adding 10 to 0xc000018030 might not be exactly what we need. This is where dereferencing memory is useful.

We can, using the pointer, deference the memory address into the variable it points to, then do the math. We can see this in the above code snippet on line 14:

*number = *number + 10 

Here, we are dereferencing our memory address to 0, then adding 10 to it.

Now the code example should work as initially expected. We share a single variable that changes are reflected against, and not by copying the value.

There are some extensions on the mental model we have created that will be helpful to understand pointers further.

Using nil pointers in Go

Everything in Go is given a 0 value when first initialized.

For example, when you create a string, it defaults to an empty string ("") unless you assign something to it.

Here are all the zero values:

  • 0 for all int types
  • 0.0 for float32, float64, complex64, and complex128
  • false for bool
  • "" for string
  • nil for interfaces, slices, channels, maps, pointers, and functions

This is the same for pointers. If you create a pointer but don’t point it to any memory address, it will be nil.

package main

import (
  "fmt"
)

func main() {
  var pointer *string
  fmt.Println(pointer) // <nil>
}

Using and dereferencing pointers

package main

import (
  "fmt"
)

func main() {
  var ageOfSon = 10
  var levelInGame = &ageOfSon
  var decade = &levelInGame

  ageOfSon = 11
  fmt.Println(ageOfSon)
  fmt.Println(*levelInGame)
  fmt.Println(**decade)
}

You can see here we were trying to re-use the ageOfSon variable in many places in our code, so we can just keep pointing things to other pointers.

But on line 15, we have to dereference one pointer, then dereference the next pointer it was pointing to.

This is utilizing the operator we already know, *, but it is also chaining the next pointer to be dereferenced, too.

This may seem confusing, but it will help that you have seen this ** syntax before when you look at other pointer implementations.

Creating a Go pointer with an alternate pointer syntax

The most common way to create pointers is to use the syntax that we discussed earlier. But there is also alternate syntax you can use to create pointers using the new() function.

Let’s look at an example code snippet.

package main

import (
  "fmt"
)

func main() {
  pointer := new(int) // This will initialize the int to its zero value of 0
  fmt.Println(pointer) // Aha! It's a pointer to: 0xc000018030
  fmt.Println(*pointer) // Or, if we dereference: 0
}

The syntax is only slightly different, but all of the principles we have already discussed are the same.

Common Go pointer misconceptions

To review everything we’ve learned, there are some often-repeated misconceptions when using pointers that are useful to discuss.

One commonly repeated phrase whenever pointers are discussed is that they’re more performant, which, intuitively, makes sense.

If you passed a large struct, for example, into multiple different function calls, you can see how copying that struct multiple times into the different functions might slow down the performance of your program.

But passing pointers in Go is often slower than passing copied values.

This is because when pointers are passed into functions, Go needs to perform an escape analysis to work out whether the value needs to be stored on the stack or in the heap.

Passing by value allows all the variables to be stored on the stack, which means garbage collection can be skipped for that variable.

Check out this example program here:

func main() {
  a := make([]*int, 1e9)

  for i := 0; i < 10; i++ {
    start := time.Now()
    runtime.GC()
    fmt.Printf("GC took %s\n", time.Since(start))
  }

  runtime.KeepAlive(a)
}

When allocating one billion pointers, the garbage collector can take over half a second. This is less than a nanosecond per pointer. But it can add up, especially when pointers are used this heavily in a huge codebase with intense memory requirements.

If you use the same code above without using pointers, the garbage collector can run more than 1,000 times faster.

Please test the performance of your use cases, as there are no hard and fast rules. Just remember the mantra, “Pointers are always faster,” is not true in every scenario.

Conclusion

I hope this has been a useful summary. In it, we covered what Go pointers are, different ways they can be created, what problems they solve, as well as some issues to be aware of in their use cases.

When I first learned about pointers, I read a multitude of well-written, large codebases on GitHub (like Docker for example) to try and understand when and when not to use pointers, and I encourage you to do the same.

It was very helpful to consolidate my knowledge and understand in a hands-on way the different approaches teams take to use pointers to their fullest potential.

There are many questions to consider, such as:

  • What do our performance tests indicate?
  • What is the overall convention in the wider codebase?
  • Does this make sense for this particular use case?
  • Is it simple to read and understand what is happening here?

Deciding when and how to use pointers is on a case-by-case basis, and I hope you now have a thorough understanding of when to best utilize pointers in your projects.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Kealan Parr Software engineer, technical writer and member of the Unicode Consortium.

Leave a Reply