Slicing and Aliasing

Sub-slices share the same backing array, so a write or an append through one slice can change what another slice sees.

Syntaxs[low:high] // length high-low, capacity to end of array; s[low:high:max] caps it

s[low:high] makes a new slice over the same elements. It starts at index low and ends just before high, so its length is high - low. Nothing is copied. Both slices point into the same array, and that sharing is called aliasing.

Why it surprises people

A write through the sub-slice changes the original. That part is usually expected. The capacity is the part that is not: a sub-slice's capacity runs from low to the end of the backing array, not to high. So s[2:4] of a six-element slice has length 2 and capacity 4. Appending to it has room, so it writes over s[4] instead of allocating. A function handed a sub-slice can corrupt elements it was never shown.

Cutting that off

A three-index slice, s[low:high:max], sets the capacity too: the capacity becomes max - low. With s[2:4:4] the capacity equals the length, so the first append has no room and must allocate a fresh array. This form has been in the language since Go 1.2 and is the way to hand out a window nobody can grow into your data.

Taking a real copy

copy(dst, src) copies elements and returns how many it copied, which is the smaller of the two lengths. The longhand is d := make([]T, len(s)); copy(d, s); slices.Clone(s), added in Go 1.21, is shorthand for exactly that. Both give a slice that shares nothing.

What trips people up

A tiny sub-slice keeps its whole backing array alive in memory. Slicing four bytes out of a one-megabyte read holds that megabyte until the sub-slice is gone.

Example

Example · go
package main

import "fmt"

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

	// Elements 2 and 3. Capacity reaches to the end of the array, so 4.
	middle := all[2:4]
	fmt.Println(middle, len(middle), cap(middle))

	// Writing through middle writes all, because the array is shared.
	middle[0] = 99
	fmt.Println(all)

	// There is spare capacity, so this append overwrites all[4].
	middle = append(middle, 42)
	fmt.Println(all)

	// A three-index slice caps capacity at the length, forcing a copy.
	safe := all[2:4:4]
	safe = append(safe, 7)
	fmt.Println(all, safe)

}

// Output:
// [2 3] 2 4
// [0 1 99 3 4 5]
// [0 1 99 3 42 5]
// [0 1 99 3 42 5] [99 3 7]

When to use it

  • Split a parsed buffer into header and body views without copying bytes.
  • Delete an element from a list by index.
  • Return a read-only looking window over an internal buffer from a library.
  • Keep a small parsed field from a large request without holding the request's memory.

More examples

Removing an element, longhand and shorthand

Both forms shift elements inside the existing array, so any other slice over that array sees the shift. Prefer slices.Delete in new code, and recognise the append form in older code.

Example · go
package main

import (
	"fmt"
	"slices"
)

func main() {
	a := []string{"a", "b", "c", "d"}
	// Longhand: everything after index 1 is shifted one place left.
	a = append(a[:1], a[2:]...)
	fmt.Println(a, len(a))

	b := []string{"a", "b", "c", "d"}
	// Shorthand for the same shift. slices.Delete removes b[1:2].
	// The slices package was added in Go 1.21.
	b = slices.Delete(b, 1, 2)
	fmt.Println(b, len(b))

}

// Output:
// [a c d] 3
// [a c d] 3

A window keeps the whole buffer alive

This is a real source of memory growth in servers that keep a small field parsed out of each large request. If the small part outlives the request, copy it.

Example · go
package main

import "fmt"

// Returns a view. Its backing array is still the whole input.
func head(big []byte) []byte {
	return big[:4]
}

// Returns a copy, so the big array can be freed once the caller drops it.
func headCopy(big []byte) []byte {
	out := make([]byte, 4)
	copy(out, big)
	return out
}

func main() {
	big := make([]byte, 1<<20) // one megabyte
	copy(big, "HTTP/1.1 200 OK")

	window := head(big)
	owned := headCopy(big)

	// cap shows how much memory each result is holding onto.
	fmt.Println(string(window), cap(window))
	fmt.Println(string(owned), cap(owned))

}

// Output:
// HTTP 1048576
// HTTP 4

Handing out a sub-slice that cannot grow into yours

Use the three-index form whenever you return a slice of your own buffer from a package or a method. It stops the caller's append from silently editing data they do not own.

Example · go
package main

import "fmt"

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

	loose := buf[0:2]   // capacity 6: an append writes buf[2]
	tight := buf[0:2:2] // capacity 2: an append must allocate

	loose = append(loose, 99)
	fmt.Println(buf)

	tight = append(tight, 77)
	fmt.Println(buf, tight)

}

// Output:
// [1 2 99 4 5 6]
// [1 2 99 4 5 6] [1 2 77]

Discussion

  • Be the first to comment on this lesson.