Go Slice Gotcha

June 11, 2019

Go is, mostly, a straightforward and unsurprising language.

One thing that caught me recently was the behavior of slices.

The setup

nums := []int{3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
chunk := nums[3:6]
fmt.Println("chunk:", chunk)   // chunk: [6 7 8]

A slice and a subslice; no surprises yet.

chunk = append(chunk, 999)

What are the consequences of this append? Spoilers ahead!

Oh no…

fmt.Println("nums: ", nums)    // nums:  [3 4 5 6 7 8 999 10 11 12 13 14]
fmt.Println("chunk:", chunk)   // chunk: [6 7 8 999]

chunk contains what you would expect.

What about that 999 after 8 in the original slice?!

Slice Capacity

The answer is not the len, check the cap

len(nums[3:6])   // 3
cap(nums[3:6])   // 9

Why 9? Because the extra capacity of the original slice was carried over when slicing. There were 9 elements from index 3 to the end of the slice:

fmt.Println(nums[3:])   // [6 7 8 9 10 11 12 13 14]
len(nums[3:])           // 9

The Solution

Go has a syntax to limit the cap when subslicing:

len(nums[3:6:6])   // 3
cap(nums[3:6:6])   // 3

It’s worth reading Slice expressions; especially the “Full slice expressions” subsection.

a[low : high : max]

I found that Go in Action (section 4.2.3) had a great explanation on this:

The purpose [of the third index] is not to increase capacity, but to restrict the capacity.

You’re still sharing the underlying array, within the valid indices. If you try to append, it’s a copy on write.

Conclusion

I’m just surprised this syntax doesn’t work the other way around:

- nums[3:6]                 // gets only what you asked for?
- nums[3:6:cap(nums) - 3]   // gets you that extra capacity?

Ugly? Yes. But I like syntactic vinegar when doing things that aren’t straightforward.

Discuss on Twitter