While writing Go, you might might run into the following situation: You want to collect the results of a function in a slice. However, the function returns a pointer.
You might then ask yourself:
What kind of slice should I use? A slice of values or a slice of pointers?
Let’s make this a bit more concrete.
Suppose the following Album
struct and ProduceAlbum
function exist:
type Album struct {
Title string
Artist string
}
// ProduceAlbum produces a random album. The details
// don't really matter in this article, in real code this might
// do an expensive calculation or retrieve data from a database.
func ProduceAlbum() *Album {
vol := rand.Intn(100) + 1
return &Album{
Title: fmt.Sprintf("Groovy vol. %d", vol),
Artist: "👨🎤",
}
}
Every time ProduceAlbum
is called, it returns a pointer to a randomly titled Album
.
Let’s say our goal is to collect 10 albums in a slice. We can do this in a loop like this:
// define a slice
for i := 0; i < 10; i++ {
// call ProduceAlbum.
// append the result to the slice.
}
Now the question is, what type of slice should we use to collect the results?
Do we define a slice of values: []Album
?
Or, do we define a slice of pointers: []*Album
?
package main
import "fmt"
func main() {
// slice of values
v := make([]Album, 0, 10)
for i := 0; i < 10; i++ {
a := ProduceAlbum()
v = append(v, *a)
}
fmt.Println(v)
// slice of pointers
p := make([]*Album, 0, 10)
for i := 0; i < 10; i++ {
p = append(p, ProduceAlbum())
}
fmt.Println(p)
}
package main
import (
"fmt"
"math/rand"
)
type Album struct {
Title string
Artist string
}
// ProduceAlbum produces a random album. The details
// don't really matter in this article, in real code this might
// do an expensive calculation or retrieve data from a database.
func ProduceAlbum() *Album {
vol := rand.Intn(100) + 1
return &Album{
Title: fmt.Sprintf("Vol. %d", vol),
Artist: "👨🎤",
}
}
In this article we will examine the differences between the two options and go over reasons why you might choose one or the other.
Slice of pointers vs slice of values
Let’s ignore the ProduceAlbum
function for now, and see how slices of values and slices of pointers compare structurally.
We begin with a slice of values called v
:
// v is a slice of values.
v := []Album{
{"Val. 1", "👨🎤"},
{"Val. 2", "🧑🎤"},
{"Val. 3", "👩🎤"},
}
If we diagram v
, it looks like this:
As you can see, the Album
values are not stored directly in v
, but in a backing array of type [3]Album
. v
only has a reference to this array.
It explains exactly how arrays and slices are related.
Now let’s look at a slice of pointers p
:
a1 := Album{"Ptr. 1", "👨🎤"}
a2 := Album{"Ptr. 2", "🧑🎤"}
a3 := Album{"Ptr. 3", "👩🎤"}
p := []*Album{&a1, &a2, &a3}
p
will also have a backing array. But instead of storing Album
values, it contains pointers to Album
variables. In other words, its elements will be of type *Album
.
When we diagram p
, you can see that the pointers add an “extra layer” of references.
Accessing the values requires us to cross this “extra layer”.
Accessing values
To access elements in a slice you can use an index expression.
If we access an element on v
, we get an Album
value.
fmt.Println(v[1]) // prints "{Val. 2 🧑🎤}"
However, if we do the same for p
, we will print a pointer.
fmt.Println(p[1]) // prints "&{Ptr. 2 🧑🎤}"
&
symbol. The fmt
package does some extra work for pointers to structs and will try to print values as well.
If you use fmt.Printf("%p\n", p[1])
you will print the actual value of the pointer itself.
To access the value the pointer points to, we need to dereference the pointer using the *
operator.
fmt.Println(*p[1]) // prints "{Ptr. 2 🧑🎤}"
Try it yourself below.
package main
import "fmt"
func main() {
// v is a slice of values.
v := []Album{
{"Val. 1", "👨🎤"},
{"Val. 2", "🧑🎤"},
{"Val. 3", "👩🎤"},
}
fmt.Println(v[1])
// p is a slice of pointers.
a1 := Album{"Ptr. 1", "👨🎤"}
a2 := Album{"Ptr. 2", "🧑🎤"}
a3 := Album{"Ptr. 3", "👩🎤"}
p := []*Album{&a1, &a2, &a3}
fmt.Println(p[1])
fmt.Printf("%p\n", p[1])
}
package main
type Album struct {
Title string
Artist string
}
When you access fields or methods on a variable, Go will automatically dereference or create a pointer as necessary.
For example, printing v[1].Title
in the above code works for both slices.
Nil pointers
It’s possible for a pointer not to point to a variable, it’s value will then be nil
.
nil
is the zero value for pointer types: if you don’t assign a value to a pointer type it will be nil
.
Dereferencing a nil
pointer will lead to a panic:
panic: runtime error: invalid memory address or nil pointer dereference
To prevent such panics, you will need to check if a pointer does not equal nil
before you dereference it.
In the example below ProduceAlbum
has been modified to return nil
every second call. Remove the nil
check to verify that the program panics.
package main
import "fmt"
func main() {
// slice of values
v := make([]Album, 0, 10)
for i := 0; i < 10; i++ {
a := ProduceAlbum()
if a != nil {
v = append(v, *a)
}
}
fmt.Println(v)
}
package main
import (
"fmt"
"math/rand"
)
type Album struct {
Title string
Artist string
}
var calls = 0
// ProduceAlbum produces a random album or returns nil.
func ProduceAlbum() *Album {
calls++
if calls%2 == 0 {
return nil
}
vol := rand.Intn(100) + 1
return &Album{
Title: fmt.Sprintf("Vol. %d", vol),
Artist: "👨🎤",
}
}
nil
you will not need nil
checks.
Be careful though, code evolves over time and nil
can easily sneak in. The Go type system will not protect you here.
Range and modify
To iterate over slices you can use a for
loop with a range
clause.
When you iterate over a slice of values, the iteration variables will be copies of those values. Any modifications you make to the iteration variables won’t be reflected outside of the loop.
For example, if we range over v
and modify the title of the iteration variable a
:
for _, a := range v {
// a is of type Album
a.Title = "Unknown album"
}
fmt.Println(v[0].Title) // prints "Groovy vol. X"
The titles of all elements in v
will remain untouched, because we only modified the iteration variable a
.
To modify the elements in v
, we need to assign a
to an element:
for i, a := range v {
// a is of type Album
a.Title = "Unknown album"
v[i] = a // assign a to an element in v.
}
fmt.Println(v[0].Title) // prints "Unknown album"
Or, alternatively, modify the elements of v
directly:
for i := range v {
v[i].Title = "Unknown album"
}
fmt.Println(v[0].Title) // prints "Unknown album"
With a slice of pointers, the iteration variables will be copies of the pointers. These copies will still point to the same values.
Changes to these values will be visible outside of the loop.
for _, a := range p {
// a is of type *Album
a.Title = "Unknown album"
}
fmt.Println(v[0].Title) // prints "Unknown album"
Appending
If we go back to our original situation, ProduceAlbum
returns a pointer.
When we append to a slice of values, we will need to dereference the pointer before we can append it.
Since we wrote it, we know that the ProduceAlbum
function will always return a non-nil
pointer. However, as we noted earlier, in real code you can’t always be so sure.
To prevent a “nil pointer dereference” panic you would again need a nil
check:
v := make([]Album, 0)
for i := 0; i < 10; i++ {
ptr := ProduceAlbum()
if ptr != nil {
v = append(v, *ptr)
}
}
Such a panic will not happen with a slice of pointers, because it can store nil
pointers just fine.
p := make([]*Album, 0)
for i := 0; i < 10; i++ {
p = append(p, ProduceAlbum())
}
Depending on your situation you might still want to filter out nil
values though.
Choosing a type
This section contains my thoughts on how to choose between slices of values and slices of pointers.
These are not hard and fast rules. If you’re in doubt, whip up a prototype with both options and play around for a bit. See what is best for your situation.
Default to slices of values
I default to using slices of values.
With slices of pointers, the Go type system will allow nil
values in the slice. This does not add any benefit unless my program requires elements to have “no value”.
Slices already consist of a reference to an array. Slices of pointers add an extra “layer of indirection”, making it more work to reason about what is going on.
Should the function return a pointer?
This also raises the question, why does ProduceAlbum
return a pointer to begin with? If possible I would probably change it to return an Album
.
However, that isn’t always possible:
- The function might be part of a third party package.
- The function might be called in a lot of places and require extensive refactoring to change its return value type.
- The
Album
struct might be very large and returning a pointer is a performance optimization.
Functionality
Sometimes you explicitly require pointers for your desired functionality.
For example, is ProduceAlbum
returning an album as a “final result”? Then Album
is a more suitable data type.
Or, is ProduceAlbum
returning an album that will automatically get refreshed by new data? Then *Album
is likely a more suitable data type.
Representing no value
If we need to differentiate between a zero value and no value in a slice, a slice of pointers can be helpful. nil
can represent an element having no value.
Consistency in receivers
If the element type of the slice has methods defined on it, you might want to match the type of the receiver.
For example, suppose we had a method defined on *Album
like this:
func (a *Album) MakeItMetal() {
a.Title = strings.ToUpper(a.Title)
a.Artist = "🧛♂️"
}
You might want to be consistent and match the a *Album
receiver and use slices of pointers ([]*Album
).
I generally don’t let this hold to much sway though.
As we saw earlier, Go provides some help with methods and fields. Calling MakeItMetal
on a variable of type Album
will work even though it is defined with an *Album
receiver.
Performance
When I first started out in Go I thought “pointers are faster” so assumed that slices of pointers were always a good idea.
This is not true. Values referenced by pointers often require more work by the Go runtime to access, allocate and clean up.
If you’re “just” passing around a slice, your program you will generally have better performance by using a slice of values.
However, it can make sense to use a slice of pointers if you’re working with a lot of large structs and pass them around individually as well.
Always use benchmarks to make an informed choice when you’re doing anything performance sensitive.
That’s it
I hope this article gives helps you choose between slices of pointers and values in your code.
If you have any questions or comments feel free to reach out to me :)
P.S. I would probably use a slice of values in the situation in the intro of this article.
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."