Have you ever wonder when to use *[]struct and when to use []*struct when developing in Go? Look no further! I’ll explain everything you need to know about this topic in this post.

Conclusion

If the underlying array of your slice is big enough for your need, there is no point at using slice of pointers, as it will need to allocate the a new piece of memory for the pointer for each entry in the slice.

However, if the your your slice will outgrow the underlying array when being appended, a new array with sufficient length will be allocated, copying all the data from the original array to the new one. This is when using slice of pointers is a better option, since only the pointers will need to be copied, not the entire values.

I don’t believe it

To test the performance difference yourself:

main_test.go

package main

import (
	"testing"
)

type SmallStruct struct {
	A int
	B int
}

const (
	SLICE_LEN = 100
	ARRAY_LEN = 100
)

func BenchmarkSliceOfSmallStructs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		slice := make([]SmallStruct, 0, ARRAY_LEN)
		for j := 0; j < SLICE_LEN; j++ {
			slice = append(slice, SmallStruct{A: j, B: j + 1})
		}
	}
}

func BenchmarkSliceOfPointersOfSmallStructs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ { // test count
		slice := make([]*SmallStruct, 0, ARRAY_LEN)
		for j := 0; j < SLICE_LEN; j++ {
			slice = append(slice, &SmallStruct{A: j, B: j + 1})
		}
	}
}

type BigStruct struct {
	F1, F2, F3, F4, F5, F6, F7                string
	I1, I2                                    int
	I3, I4, I5, I6, I7, I8, I9, I10, I11, I12 int
	A1, A2, A3, A4, A5, A6, A7                SmallStruct
	A8, A9, A10, A11, A12, A13, A14           SmallStruct
}

func BenchmarkSliceOfBigStructs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		slice := make([]BigStruct, 0, ARRAY_LEN)
		for j := 0; j < SLICE_LEN; j++ {
			slice = append(slice, BigStruct{
				I1: j,
				I2: j + 1,
			})
		}
	}
}

func BenchmarkSliceOfPointersOfBigStructs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		slice := make([]*BigStruct, 0, ARRAY_LEN)
		for j := 0; j < SLICE_LEN; j++ {
			slice = append(slice, &BigStruct{
				I1: j,
				I2: j + 1,
			})
		}
	}
}

To see the direct outputs

go test -bench . -count 3

To see analyzed outputs, install benchstat first

go install golang.org/x/perf/cmd/benchstat@latest

and then

go test -bench . -count 10 -benchmem >> benchmark.txt
benchstat benchmark.txt 

(You don’t need to add the -benchmen flag if you already have b.ReportAllocs() in your test functions actually.)

Output when SLICE_LEN = 100, ARRAY_LEN = 100:

goos: darwin
goarch: amd64
pkg: gslice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
                                 │ benchmark.txt │
                                 │    sec/op     │
SliceOfSmallStructs-12               89.89n ± 4%
SliceOfPointersOfSmallStructs-12     2.133µ ± 8%
SliceOfBigStructs-12                 3.363µ ± 7%
SliceOfPointersOfBigStructs-12       8.597µ ± 3%
geomean                              1.534µ

                                 │ benchmark.txt  │
                                 │      B/op      │
SliceOfSmallStructs-12               0.000 ± 0%
SliceOfPointersOfSmallStructs-12   1.562Ki ± 0%
SliceOfBigStructs-12                 0.000 ± 0%
SliceOfPointersOfBigStructs-12     43.75Ki ± 0%
geomean                                         ¹
¹ summaries must be >0 to compute geomean

                                 │ benchmark.txt │
                                 │   allocs/op   │
SliceOfSmallStructs-12              0.000 ± 0%
SliceOfPointersOfSmallStructs-12    100.0 ± 0%
SliceOfBigStructs-12                0.000 ± 0%
SliceOfPointersOfBigStructs-12      100.0 ± 0%
geomean                                        ¹
¹ summaries must be >0 to compute geomean

We can see that slice of structs is better than slice of pointers in every aspect, by a mile.

Output when SLICE_LEN = 100, ARRAY_LEN = 10:

benchstat benchmark_raw.txt > benchmark.txt
cat benchmark.txt
goos: darwin
goarch: amd64
pkg: gslice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
                                 │ benchmark_raw.txt │
                                 │      sec/op       │
SliceOfSmallStructs-12                  880.6n ± 17%
SliceOfPointersOfSmallStructs-12        2.815µ ±  7%
SliceOfBigStructs-12                    20.41µ ± 15%
SliceOfPointersOfBigStructs-12          9.213µ ±  3%
geomean                                 4.646µ

                                 │ benchmark_raw.txt │
                                 │       B/op        │
SliceOfSmallStructs-12                  4.812Ki ± 0%
SliceOfPointersOfSmallStructs-12        3.906Ki ± 0%
SliceOfBigStructs-12                    147.2Ki ± 0%
SliceOfPointersOfBigStructs-12          46.09Ki ± 0%
geomean                                 18.90Ki

                                 │ benchmark_raw.txt │
                                 │     allocs/op     │
SliceOfSmallStructs-12                    4.000 ± 0%
SliceOfPointersOfSmallStructs-12          104.0 ± 0%
SliceOfBigStructs-12                      4.000 ± 0%
SliceOfPointersOfBigStructs-12            104.0 ± 0%
geomean                                   20.40

We can see that slice of pointers use less time and memory then slice of structs when it has to constantly reallocating new arrays and copying old entries, especially when each entry is a big struct.

References

Read Arrays, slices (and strings): The mechanics of ‘append’ | Go Blog. You’ll understand everything after reading this official blog post thoroughly. You can also read my note about slice in Go.

Some discussions containing partial truths: