Slices

A slice is a resizable view over an array, carrying a pointer to the elements plus a length and a capacity.

Syntaxs := make([]T, length, capacity) // len(s), cap(s), s[i], s[:]

A slice is the normal way to hold a list in Go. It is written []int: square brackets with no number in them. Unlike an array, a slice has no length in its type, so one []int variable can hold two elements today and two thousand later.

What a slice actually is

A slice does not hold the elements itself. It holds three small pieces of information: the address of the first element inside some array, a length (how many elements this slice covers) and a capacity (how many elements there is room for, counting from that first element to the end of the array). len(s) reads the length and cap(s) reads the capacity. The array underneath is called the backing array, and you normally never name it.

Because a slice is a view, two slices can look at the same elements. Writing through one is visible through the other.

Making one

s := []string{"a", "b"} builds an array of two strings and hands you a slice over it. make([]int, 3) gives three zeros, and make([]int, 0, 10) gives a slice of length 0 with room for 10. a[:] is shorthand for a[0:len(a)] and gives a slice over a whole array. Both forms mean the same thing.

What trips people up

The zero value of a slice is nil, not an empty array. A nil slice is still safe: len, cap, range and append all work on it. But slices cannot be compared with == at all; the only comparison allowed is against nil.

Example

Example · go
package main

import "fmt"

func main() {
	// A slice literal. No number in the type: just []string.
	names := []string{"ada", "alan", "grace"}
	fmt.Println(names, len(names), cap(names))

	// make takes a length and, optionally, a larger capacity.
	buf := make([]int, 2, 5)
	fmt.Println(buf, len(buf), cap(buf))

	// A slice is a view. Writing through the view writes the shared elements.
	first := names[0:2]
	first[0] = "ADA"
	fmt.Println(names, first)

	// The zero value of a slice is nil, and nil slices still work.
	var empty []int
	fmt.Println(empty == nil, len(empty), cap(empty))

}

// Output:
// [ada alan grace] 3 3
// [0 0] 2 5
// [ADA alan grace] [ADA alan]
// true 0 0

When to use it

  • Hold a list of database rows whose count is only known at run time.
  • Read a request body into a reusable byte buffer.
  • Pass part of a larger list to a function without copying the elements.
  • Build up results in a loop when the final count is unknown.

More examples

nil slice against empty slice

Both behave the same for reading, ranging and appending, so prefer declaring var s []T and letting append allocate. The difference matters only when something checks for nil, such as JSON encoding, which writes null for a nil slice and [] for an empty one.

Example · go
package main

import "fmt"

func main() {
	var fromNil []string // nil: no backing array yet
	empty := []string{}  // not nil, but length 0

	fmt.Println(fromNil == nil, empty == nil)
	fmt.Println(len(fromNil), len(empty))

	// append works on a nil slice: it allocates the first array for you.
	fromNil = append(fromNil, "first")
	fmt.Println(fromNil, len(fromNil))

}

// Output:
// true false
// 0 0
// [first] 1

What a function can and cannot change

This is the single most common slice misunderstanding. Element writes travel back to the caller; length changes do not. Any function that grows a slice must return it.

Example · go
package main

import "fmt"

// The three pieces are copied, but they point at the same elements,
// so this write is visible to the caller.
func setFirst(xs []int) {
	xs[0] = 100
}

// Growing changes the length, which lives in this function's own copy,
// so the new slice has to be returned.
func addOne(xs []int) []int {
	return append(xs, 1)
}

func main() {
	xs := []int{1, 2, 3}

	setFirst(xs)
	fmt.Println(xs)

	ys := addOne(xs)
	fmt.Println(len(xs), len(ys))

}

// Output:
// [100 2 3]
// 3 4

Reserving capacity up front

When you already know how many results you will produce, make the slice with length 0 and that capacity. The loop then does no extra allocation. Reach for this in any loop that builds one output per input row.

Example · go
package main

import "fmt"

func main() {
	words := []string{"go", "is", "small"}

	// Length 0, room for one result per word: no regrowing inside the loop.
	lengths := make([]int, 0, len(words))
	for _, w := range words {
		lengths = append(lengths, len(w))
	}

	fmt.Println(lengths, len(lengths), cap(lengths))

}

// Output:
// [2 2 5] 3 3

Discussion

  • Be the first to comment on this lesson.