Skip to the content.

slice pitfalls

A slice is a reference to an underlying array. If two slices share the underlying array, changing one slice MAY affects another one.

func printSlice(s []int) {
	fmt.Printf("len=%d, cap=%d, s=%v\n", len(s), cap(s), s)
}

func main() {
	s1 := []int{1}
	s2 := s1
	printSlice(s1) // len=1, cap=1, s=[1]

	s1 = append(s1, 2)
	s2 = append(s2, 5) // <--- it will not change s1
	printSlice(s1)     // len=2, cap=2, s=[1 2]
	printSlice(s2)     // len=2, cap=2, s=[1 5]

	s1 = append(s1, 3, 4, 5, 6, 7)
	s2 = s1
	printSlice(s1) // len=7, cap=8, s=[1 2 3 4 5 6 7]
	printSlice(s2) // len=7, cap=8, s=[1 2 3 4 5 6 7]

	s1 = append(s1, 8)
	printSlice(s1)     // len=8, cap=8, s=[1 2 3 4 5 6 7 8]
	printSlice(s2)     // len=7, cap=8, s=[1 2 3 4 5 6 7]

	s2 = append(s2, 9) // <---- it will change s1
	printSlice(s1)     // len=8, cap=8, s=[1 2 3 4 5 6 7 9]
	printSlice(s2)     // len=8, cap=8, s=[1 2 3 4 5 6 7 9]

}

The build-in function append will append values to the slice and may realloc underlying array for that:

If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values. Otherwise, append re-uses the underlying array.

The KEY POINT is that, the new array may have some “extra” space for furture use. If the user only appends one value to the original slice, it will re-use the underlying array, i.e., use that “extra” space. And then, if the user only appends one value to the “copied” slice, it will also use that “extra” space, i.e., new value overwrites that space.

Another example is the Record struct in standard pkg slog. A Record holds information about a log event. Copies of a Record share state. As its doc said:

Do not modify a Record after handing out a copy to it. Call NewRecord to create a new Record. Use Record.Clone to create a copy with no shared state, and then the original record and the clone can both be modified without interfering with each other.

The following code shows that, the assignment operation makes a copy of original record and modifying one record will affect another.


func main() {

	/* The following code uses assignment to create a copy with
	 * shared state, and then the original record and the copy
	 * may interfer with each other.
	 */
	r1 := slog.NewRecord(time.Now(), slog.LevelDebug, "hello", 0)
	r1.Add("s1", 1, "s2", 2, "s3", 3, "s4", 4, "s5", 5)
	r1.Add("b1", 1)
	r1.Add("b2", 2, "b3", 3, "b4", 4, "b5", 5, "b6", 6, "b7", 7)
	r2 := r1

	s1 := []slog.Attr{}
	s2 := []slog.Attr{}
	gets1 := func(a slog.Attr) bool {
		s1 = append(s1, a)
		return true
	}
	gets2 := func(a slog.Attr) bool {
		s2 = append(s2, a)
		return true
	}
	r1.Attrs(gets1)
	r2.Attrs(gets2)

	fmt.Printf("r1 last attr: %v\n", s1[len(s1)-1]) // r1 last attr: b7=7
	fmt.Printf("r2 last attr: %v\n", s2[len(s2)-1]) // r2 last attr: b7=7

	r1.Add("b8", 8)
	s1 = []slog.Attr{}
	s2 = []slog.Attr{}
	r1.Attrs(gets1)
	r2.Attrs(gets2)
	fmt.Printf("r1 last attr: %v\n", s1[len(s1)-1]) // r1 last attr: b8=8
	fmt.Printf("r2 last attr: %v\n", s2[len(s2)-1]) // r2 last attr: b7=7

	r2.Add("b8", 99999) // <--- it will change r1
	s1 = []slog.Attr{}
	s2 = []slog.Attr{}
	r1.Attrs(gets1)
	r2.Attrs(gets2)
	fmt.Printf("r1 last attr: %v\n", s1[len(s1)-1]) // r1 last attr: b8=99999
	fmt.Printf("r2 last attr: %v\n", s2[len(s2)-1]) // r2 last attr: b8=99999

	/* The following code uses Record.Clone to create a copy with
	 * no shared state, and then the original record and the clone
	 * can both be modified without interfering with each other.
	 */
	r1 = slog.NewRecord(time.Now(), slog.LevelDebug, "hello", 0)
	r1.Add("s1", 1, "s2", 2, "s3", 3, "s4", 4, "s5", 5)
	r1.Add("b1", 1)
	r1.Add("b2", 2, "b3", 3, "b4", 4, "b5", 5, "b6", 6, "b7", 7)
	r2 = r1.Clone()

	r1.Add("b8", 8)
	r2.Add("b8", 99999) // <--- it will not change r1

	s1 = []slog.Attr{}
	s2 = []slog.Attr{}
	r1.Attrs(gets1)
	r2.Attrs(gets2)
	fmt.Printf("r1 last attr: %v\n", s1[len(s1)-1]) // r1 last attr: b8=8
	fmt.Printf("r2 last attr: %v\n", s2[len(s2)-1]) // r2 last attr: b8=99999
}

If we dig into the details of Record.Add() method, we will find that append is used for assembling the slice.

type Record struct {
	Time time.Time
	Message string
	Level Level
	PC uintptr

	// It holds the start of the list of Attrs.
	front [nAttrsInline]Attr

	// The number of Attrs in front.
	nFront int

	// The list of Attrs except for those in front.
	back []Attr
}

func (r *Record) Add(args ...any) {
	// ..
	// other codes

	if r.back == nil {
		r.back = make([]Attr, 0, countAttrs(args)+1)
	}
	r.back = append(r.back, a)
}

If we dig into the details of Record.Clone() method, will find that the slice will be “re-sliced” and remove extra space in the underlying array. When the user adds a new attribute, append will allocate a new array to store data. Thus original slice and the clone will have distinct underlying arrays. It is the mechnism of “cloning”.

func (r Record) Clone() Record {
	r.back = slices.Clip(r.back) // prevent append from mutating shared array
	return r
}

//in pkg slices
// Clip removes unused capacity from the slice, returning s[:len(s):len(s)].
func Clip[S ~[]E, E any](s S) S {
	return s[:len(s):len(s)]
}