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)
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.
build your own slice series of articles.
If you want to know more about how slices work and are constructed, check out myBy 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).
This is not just a conceptual issue. This extra layer of indirection will make your code more complex.
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.
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:
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
.
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:
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 :)
Get my free newsletter every second week
Used by 500+ developers to boost their Go skills.
"I'll share tips, interesting links and new content. You'll also get a brief guide to time for developers for free."