Thanks to Julia Evan’s latest post, I learned that creating new slices by sub-slicing an existing slice has an important caveat: They can sometimes use the same backing array! 😬

This is important to understand if you mutate the sub-slice.

Take the below example, wherein we accidentally mutate s1!

package main

import (
    "fmt"
)

func main() {
    s1 := []int{0, 1, 2, 3, 4, 5} // len == 6, capacity == 6
    s2 := s1[1:5]                 // len == 4, capacity == 5

    // Modifies both s1 and s2, because they share the same backing array.
    s2[0] = 10

    // Modifies both s1 and s2, because the length of s2 after adding the new
    // element does not exceed the capacity of the s2 slice.
    s2 = append(s2, 20)

    // Modifies only s2, because adding this element increases the length of s2
    // beyond the capacity of the s2 slice. This allocats a new backing array,
    // copies all elements to it, and then appends 30 to it. So s1 does not get
    // modified.
    s2 = append(s2, 30)

    fmt.Println("s1", s1)
    fmt.Println("s2", s2)
}

Its output is:

s1 [0 10 2 3 4 20]
s2 [10 2 3 4 20 30]

Contrast this with the below example, where we do not mutate s1.

package main

import (
    "fmt"
)

func main() {
    s1 := []int{0, 1, 2, 3, 4, 5} 

    // Copy the sub-slice values into a slice which uses a new backing array.
    var s2 []int
    s2 = append(s2, s1[1:5]...)

    // None of the below change the value of s1 because s2 uses a different
    // backing array.
    s2[0] = 10
    s2 = append(s2, 20)
    s2 = append(s2, 30)

    fmt.Println("s1", s1)
    fmt.Println("s2", s2)
}
s1 [0 1 2 3 4 5]
s2 [10 2 3 4 20 30]