Should you use pointers to slices in Go?

Avatar of the author Willem Schots
3 Aug, 2023
~6 min.
RSS

If you’re unfamiliar with Go slices, it might look like a good idea to pass around a pointer instead of the “entire collection”.

s := []string{"👁", "👃", "👁"}
doSomething(&s)
andAgain(&s)
wrong mental model of a pointer to a slice

However, this has no real upsides.

By passing around a slice, you’re already passing around a pointer: A slice holds a pointer to an underlying array.

If you want to know more about how slices work and are constructed, check out my build your own slice series of articles.

By using a pointer to a slice you will essentially be using double pointers (a pointer to a slice, which in turn points to an array).

right mental model of a pointer to a slice

This is not just a conceptual issue. This extra layer of indirection will make your code more complex.

Are you or your team struggling with aspects of Go?

Book a 30 minute check-in with me.

I can help with questions like:

  • How should we structure our Go web app?
  • Is this package a good choice for us?
  • How do I get my team up to speed quickly?
  • Can I pick your brain on {subject}?
Learn more

Looking forward to meeting you.

- Willem

Complexity 1: The pointer can be nil

All built-in Go functions that work with slices (like append(), copy(), len() and cap()) expect slices as input. Not pointers to slices.

Before calling any of these functions you will need to dereference the pointer.

"Dereferencing" means "accessing the value" at the memory address that a pointer points to.

This is done using the * operator in Go.

You can only dereference non-nil pointers, because a nil pointer does not point to a value.

pointer points to nil

If your code dereferences a nil pointer it will panic like this:

panic: runtime error: invalid memory address or nil pointer dereference

So, before dereferencing the pointer you will need to check if it is not nil.

For example:

main.go
package main

import "fmt"

func main() {
	s := []string{"👁", "👃", "👁"}
	printLen(&s)
	printLen(nil)
}

func printLen(sPtr *[]string) {
	// before we dereference the pointer we check if it is not nil.
	if sPtr != nil {
		// dereference the pointer when getting the length.
		fmt.Printf("the length of the slice is: %d\n", len(*sPtr))
	} else {
		fmt.Println("pointer is nil")
	}
}

This will lead to a lot of extra code, and if you forget one check, you risk a panic. Not fun.

Complexity 2: The slice can be nil

Next to the pointer, the slice itself can also be nil.

slice is nil

You don’t often need to check for this (all the built-in slice functions are able to handle it), but it is sometimes necessary.

If you wan to check for this, you will first need to check if the pointer is not nil and then if slice is nil.

if sPtr != nil && *sPtr == nil {
	// do something when slice is nil.
}

When reading code like this, it takes me some (sometimes a lot) mental energy to process.

The alternative without pointers is a bit easier on the eye:

if s == nil {
	// do something when slice is nil.
}

Complexity 3: Other programmers

Unless there is a specific reason, other Go programmers don’t use pointers to slices in their code. To work together smoothly with other programmers it’s a good idea to stick to common conventions as much as possible.

When to use them

As you have seen, in general you should use slices and not pointers to slices.

But are there are some situations where pointers to slices are useful

Decoding and unmarshaling data

Probably the most common situation in which you would use a pointer to a slice is when decoding or unmarshaling data.

The gob, xml and json packages in the standard library all have functions and methods that work in a similar way:

func (d *Decoder) Decode(v any) error

func Unmarshal(data []byte, v any) error

For both, v needs to be a pointer to the variable you want to decode or unmarshal the data into. This means that if you want to decode or unmarshal data into a slice, you will need to pass a pointer to a slice.

These functions use pointers instead of accepting and returning values. When they modify the value the pointer points to, the changes will be visible to the caller.

When you pass a slice pointer to these functions they will be able to replace the slice value (by appending for example).

For example, if you want to unmarshal a json array into a slice:

main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	data := []byte("[\"👁\", \"👃\", \"👁\"]")
	var target []string
	// unmarshal needs a pointer to the target slice.
	err := json.Unmarshal(data, &target)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(target)
}

Pointer receivers for custom types

It’s possible to define a custom type that has a slice as its underlying type. For example:

type greetings []string

Defines a custom type called greetings that has a slice of strings as its underlying type.

If we want to implement a method that manipulates the slice you have two options:

  • Using a value receiver.
  • Using a pointer receiver.

Let’s say we want to append a greeting every time the AddOne method is called.

If we use a value receiver we would need to return a new greetings value from the method:

func (g greetings) AddOne() greetings {
	return append(g, "hello!")
}

And it would be up to the caller to handle the result of calling AddOne.

var g greetings
g = g.AddOne()
fmt.Println(g) // prints: [hello!]

If we use a pointer receiver, we can manipulate the receiver directly in the method and there is no need to return a new greetings value.

func (g *greetings) AddOne() {
	*g = append(*g, "hello!")
}

The caller would now look like this:

g := &greetings{}
g.AddOne()
fmt.Println(g) // prints: &[hello!]

There is no right or wrong here, it all depends on how you want your type to be used.

Conclusion

In this article we discussed why it’s generally not a good idea to use pointers to slices:

  • Need to be dereferenced, which in turn will add a lot of nil checks.
  • It makes checking if the slice is nil more complex.
  • Not commonly used by other Go programmers.

In addition we saw two situations in which pointers to slices can be useful:

  • When decoding/unmarshaling from raw data to a variable.
  • When using pointer receivers on custom types that have a slice as an underlying type.

I hope this was useful to you :)

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Twitter/X, LinkedIn or Mastodon.

Thanks for reading!