The thing about slices

Slices are tricky. If you have been using Go for a while now, you may be aware that a slice is basically a triplet consisting of a:

  1. pointer to an array of values,
  2. the capacity of the array,
  3. the length of the array

This makes working with slices also a bit different than working with structs.

The slice operator

The first thing one should be aware of is that the slice operator, does not create and copy the data to a new slice. Not taking into account this fact, taking a look at the following example would produce unexpected results:

a := []string{"r","u","n"}
b := a[1:2]
b[0] = "a"
fmt.Println(a)

Does it print [r u n] or [r a n]? Since the slice b is not a copy of the slice, but just a slice with a modified pointer to the array, the above will print [r a n].

As explained, this is because the slice operator just provides an new slice with an updated reference to the same array from the original slice. From the official reference:

Slicing does not copy the slice’s data. It creates a new slice value that points to the original array. This makes slice operations as efficient as manipulating array indices. Therefore, modifying the elements (not the slice itself) of a re-slice modifies the elements of the original slice.

Source: Go Blog - Slices usage and internals.

Allocation by append

Appending to a slice is simple enough. As mentioned, the slice has a length which you can get with a call to len(), and a capacity which you can get with a call to cap().

a := []string{"r", "u", "n"}
fmt.Println(a, len(a), cap(a))
a = append(a, []string{"e"}...)
fmt.Println(a, len(a), cap(a))
a = a[1:len(a)]
fmt.Println(a, len(a), cap(a))

The expected output would be:

[r u n] 3 3
[r u n e] 4 4
[u n e] 3 3

Of course, that’s not how append works. Append will double the existing capacity. This means you’ll end up with output like this:

[r u n] 3 3
[r u n e] 4 6
[u n e] 3 5

If you wanted the previous result, you would have to create your own function, which would allocate only the required amount of items into a new slice, and then copy over the data from the source slice. This function could look something similar to this:

func suffix(source []string, vars ...string) []string {
	length := len(source) + len(vars)
	ret := make([]string, length, length)
	copy(ret, source)
	index := len(source)
	for k, v := range vars {
		ret[index+k] = v
	}
	return ret
}

func main() {
	a := []string{"r", "u", "n"}
	fmt.Println(a, len(a), cap(a))
	a = suffix(a, []string{"e"}...)
	fmt.Println(a, len(a), cap(a))
	a = a[1:len(a)]
	fmt.Println(a, len(a), cap(a))
}

Copying slices

As discovered above, “Slicing does not copy the slice’s data.”. This may sometimes have unintended consequences. This doesn’t apply only to slicing, but also passing slices into functions.

func flip(source []string) {
	source[1] = "a"
}

func main() {
	a := []string{"r", "u", "n"}
	flip(a)
	fmt.Println(a)
}

The above example will print [r a n]. This is unfortunate, because people intuitively expect slices to behave much like structs do. Passing a struct will create a copy. A slice will still point at the same memory that holds the data. Even if you pass a slice within a struct, you should be aware of this:

type X struct {
	c string
	source []string
}

func flip(x X) {
	x.c = "b"
	x.source[1] = "a"
}

func main() {
	a := X{"a", []string{"r", "u", "n"}}
	flip(a)
	fmt.Println(a)
}

The above will print out a {a [r a n]}. Slices always behave like they are passed by reference. In a way they are, as one of the parts of the slice is a pointer to the array of data it holds.

Using slices in channels

In case you’re using buffered channels, and are trying to be smart with slices - reusing a slice to keep allocations down, then you might find that this buffered channel is basically filled with the same slice over and over. As the slice contains the pointer to the array holding the data, the following example will have unexpected results:

func main() {
	c := make(chan []string, 5)

	go func() {
		item := []string{"hello"}
		for i := 0; i < 5; i++ {
			item[0] = fmt.Sprintf("hello %d", i)
			c <- item
			//time.Sleep(100 * time.Millisecond)
		}
	}()

	for i := 0; i < 5; i++ {
		item := <-c
		fmt.Println(item)
	}
}

The output is:

[hello 4]
[hello 4]
[hello 4]
[hello 4]
[hello 4]

If you uncomment the time.Sleep command in the goroutine, you will most likely get the correct result:

[hello 0]
[hello 1]
[hello 2]
[hello 3]
[hello 4]

At any time when the consumer is reading from the channel, the retrieved slice will be identical, because only the item in the underlying array is changing. The only solve for this I believe is either finding a way to work with an unbuffered channel (a consumer must always be waiting to read from the channel), or to explicitly copy the slice which is being put on the channel, like in this playground example.

Since you’re here… I started writing my third book, The SaaS Handbook. If you enjoy this blog, or if you would like to build your own SaaS/product, consider supporting me and buying a copy. Any income from the book is going towards writing new and insightful content and teaching people how to be better programmers. Thanks :)

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.