Rent-a-founder

Golang: Append Modifies Underlying Slices

Even after four years of programming in Go, there are still things I didn’t know about the language itself. The following example illustrates behaviour that surprised me. My (false) assumptions actually led to a bug which was very difficult to find as it happened only rarely and repeated reviews of the code didn’t turn up anything unusual. Here it is:

package main

import (
	"fmt"
)

func main() {
	a := []int{1, 2, 3}
	b := append(a[:2], 4)
	fmt.Println(a)
	fmt.Println(b)
}

One would think that the output would be:

[1 2 3]
[1 2 4]

But it’s actually:

[1 2 4]
[1 2 4]

The a slice is also modified. So append does not allocate a new slice but overwrites the third element of the original slice with the new value (4). It’s not a surprise if you read the specification thoroughly:

Otherwise, append re-uses the underlying array.

The “otherwise” in this sentence is the critical part. Because if there is no space to append the new elements, a new slice is allocated and a is indeed left untouched:

package main

import (
	"fmt"
)

func main() {
	a := []int{1, 2, 3}
	b := append(a[:2], 4, 5)
	fmt.Println(a)
	fmt.Println(b)
}

The output here is:

[1 2 3]
[1 2 4 5]

For this blog post, I thought I’d justify my false assumption by pointing out that a language like Javascript behaves differently:

var a = [1,2,3],
  b = a.slice(0,2).concat(4);
console.log(a);
console.log(b);

But slice will always allocate a new array so the result is expected:

[1, 2, 3]
[1, 2, 4]

So maybe I should have seen this coming. But I think append's actual behaviour here may be surprising to some Go developers as it was to me so I’d say it warrants its own blog post.

Update Oct 15, 2018

Of course, append also affects other partial slices:

package main

import (
	"fmt"
)

func main() {
	a := []byte("abcdefghijklmnopqrstuvwxyz")
	b := a[2:4]
	c := a[:2]
	c = append(c, '0', '1')
	fmt.Println(string(b))
	// Output: 01
}

But if we do the same with strings, it’s a little different:

package main

import (
	"fmt"
)

func main() {
	a := "abcdefghijklmnopqrstuvwxyz"
	b := a[2:4]
	c := a[:2]
	c = string(append([]byte(c), '0', '1'))
	fmt.Println(b)
	// Output: cd
}

It appears that converting strings to byte slices or vice versa creates a copy. That conversion was necessary because append does not work on strings.