Arrays and Slices

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18. Every runnable program below was vetted with go vet, compiled and run with that toolchain, and the output (including compiler errors and panics) is the real output. Arrays and slices are Go’s two sequence types for elements of one type. An array is a fixed-size value; a slice is a small descriptor that views part of an array, and it is the type you reach for in almost every API. Understanding the difference, and what the slice descriptor does when you slice, append and copy, removes most of the surprises people meet when they come to Go from Java, C#, Python or Rust. The examples use the library catalogue domain (shelf) used across this reference. The following pages build on this one: Maps, Strings, Runes and UTF-8 (a string is a read-only sequence of bytes, and []byte is its mutable slice counterpart) and Pointers and Value Semantics, which explains why passing a slice by value is cheap but still shares its elements. The generic helpers for slices are introduced here and treated in full on Collections and Utility Packages.

Arrays

An array is a numbered sequence of elements of a single type. Its length is fixed when the type is declared and is part of the type: [3]int and [4]int are different, non-assignable types. The length must be a non-negative constant that is representable as an int; the elements are always initialized (to the zero value of the element type when you do not provide them), and an array occupies one contiguous block of memory with no header.

package main

import "fmt"

func main() {
	var empty [3]int                                 // zero value: every element is 0
	titles := [...]string{"Dune", "Emma", "Ulysses"} // the compiler counts the elements
	sparse := [5]int{1: 10, 3: 30}                   // indexed elements; the rest are zero
	sized := [...]string{2: "third", 0: "first"}     // length is the highest index + 1

	fmt.Println(empty, len(empty))
	fmt.Println(titles, len(titles), cap(titles))
	fmt.Println(sparse)
	fmt.Println(sized, len(sized))

	fmt.Printf("%T %T\n", empty, sparse) // [3]int and [5]int are different types
	fmt.Println(titles == [3]string{"Dune", "Emma", "Ulysses"})

	const n = len(titles) // len of an array is a constant (no function calls or receives in the expression)
	var copyOf [n]string
	copyOf = titles // assignment copies all elements
	copyOf[0] = "Changed"
	fmt.Println(titles[0], copyOf[0])
}
[0 0 0] 3
[Dune Emma Ulysses] 3 3
[0 10 0 30 0]
[first  third] 3
[3]int [5]int
true
Dune Changed

Things this example shows:

  • [N]T{...} is a composite literal. [...]T{...} asks the compiler to count the elements, so the length cannot go stale when you add one. An indexed literal such as [5]int{1: 10, 3: 30} sets selected positions, and with [...] the length becomes the highest index plus one.

  • len(a) of an array (and of a pointer to an array) is a constant when the expression contains no channel receives or function calls, so it can size another array, as [n]string does above. cap(a) equals len(a).

  • The array type is part of the value’s identity: the %T verb prints [3]int and [5]int as distinct types.

  • Arrays are comparable with == and != when their element type is comparable, and an array can be used as a map key, which a slice cannot. Two arrays are equal when all corresponding elements are equal.

  • Assigning an array copies every element. Nothing is shared afterwards.

Value semantics, pointers to arrays and range

Because an array is a value, passing it to a function, returning it, assigning it or storing it in a struct copies the whole thing. That is exactly what you want for a small fixed block such as a 16-byte hash or an RGB triple, and usually not what you want for a large one: a function that takes [1000000]int copies eight megabytes on every call. Pass a pointer to the array (*[N]T) or, much more commonly, a slice.

Go lets you index, slice and range over a pointer to an array without writing the dereference: row[0] is shorthand for (*row)[0] when row is a *[3]string. That is the only auto-dereference Go gives you for arrays, and it is what makes for i, v := range &arr possible.

package main

import "fmt"

// shelfRow is a fixed-size row of ISBN-like codes.
type shelfRow [3]string

func relabel(row shelfRow) shelfRow {
	row[0] = "relabelled" // changes the copy only
	return row
}

func relabelPtr(row *shelfRow) {
	row[0] = "relabelled" // (*row)[0]: indexing through a pointer to an array is allowed
}

func main() {
	row := shelfRow{"A1", "A2", "A3"}
	out := relabel(row)
	fmt.Println(row, out)

	relabelPtr(&row)
	fmt.Println(row)

	// Ranging over an array evaluates (copies) it once; ranging over a pointer to it does not.
	nums := [3]int{1, 2, 3}
	for i, v := range nums {
		nums[2] = 100
		if i == 2 {
			fmt.Println("range over array copy saw:", v)
		}
	}
	nums = [3]int{1, 2, 3}
	for i, v := range &nums {
		nums[2] = 100
		if i == 2 {
			fmt.Println("range over &array saw:", v)
		}
	}

	// Multi-dimensional: an array of arrays, laid out contiguously.
	var grid [2][3]int
	for r := range grid {
		for c := range grid[r] {
			grid[r][c] = r*10 + c
		}
	}
	fmt.Println(grid, len(grid), len(grid[0]))

	// A jagged structure needs slices of slices.
	jag := [][]int{{1}, {2, 3}, {4, 5, 6}}
	fmt.Println(jag, len(jag[2]))

	// Arrays as map keys (comparable), unlike slices.
	seen := map[[2]int]string{{0, 1}: "a", {1, 0}: "b"}
	fmt.Println(seen[[2]int{1, 0}])
}
[A1 A2 A3] [relabelled A2 A3]
[relabelled A2 A3]
range over array copy saw: 3
range over &array saw: 100
[[0 1 2] [10 11 12]] 2 3
[[1] [2 3] [4 5 6]] 3
b

Details worth remembering:

  • A range clause over an array evaluates the range expression once, which copies the array, so changes to later elements made inside the loop are not seen by the loop variable. Ranging over &nums (a pointer to the array) or over a slice does not copy the elements, so those changes are visible. This is the same rule the spec gives for for range; see Control Flow.

  • A multi-dimensional array such as [2][3]int is an array of arrays and is laid out contiguously, row after row. It is rectangular: every row has the same length, because the length is part of the type. For jagged data use a slice of slices ([][]int), where each row is its own slice and its own allocation.

  • A named array type such as shelfRow is a defined type and can carry methods like any other defined type. The literal shelfRow{"A1", "A2", "A3"} therefore has type shelfRow, not [3]string, though the two are assignable to each other because one of them is not a defined type.

Arrays are rare in everyday Go code. They show up as fixed-size keys and identifiers ([16]byte for a UUID, [32]byte for a SHA-256 digest from crypto/sha256), as lookup tables, as the backing storage that a slice points into, and as the key of a map. For everything else use a slice.

Compile-time and run-time errors for arrays

Constant indexes are checked by the compiler, and the array length is part of type identity:

package main

func main() {
	var a [3]int
	a[3] = 1
}
./main.go:5:4: invalid argument: index 3 out of bounds [0:3]
package main

import "fmt"

func main() {
	var a [3]int
	var b [4]int
	b = a
	fmt.Println(a, b)
}
./main.go:8:6: cannot use a (variable of type [3]int) as [4]int value in assignment

A non-constant index that is out of range cannot be caught at compile time, so it panics when it runs. That is the same for arrays and slices; see Run-time panics.

Slices

A slice describes a contiguous segment of an underlying array and provides access to a numbered sequence of the elements of that array. Unlike an array, a slice’s type ([]T) does not include a length, the length can change at run time, and a slice value is a three-word descriptor. The runtime representation is a pointer to the first element it can reach, the number of elements (len) and the number of elements from the pointer to the end of the backing array (cap).

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	backing := [6]int{10, 20, 30, 40, 50, 60}
	s := backing[1:4] // pointer to backing[1], length 3, capacity 5
	fmt.Println(s, len(s), cap(s))
	fmt.Println(unsafe.SliceData(s) == &backing[1])
	fmt.Println("header size in bytes:", unsafe.Sizeof(s)) // pointer + len + cap on a 64-bit platform

	s[0] = 99 // writes through to the shared array
	fmt.Println(backing)

	t := s[:cap(s)] // reslicing may extend up to the capacity
	fmt.Println(t, len(t))

	u := s[2:] // new header: pointer moves, len and cap shrink
	fmt.Println(u, len(u), cap(u))

	// Assigning a slice copies the header, not the elements.
	v := s
	v[1] = -1
	fmt.Println(s, v)
}
[20 30 40] 3 5
true
header size in bytes: 24
[10 99 30 40 50 60]
[99 30 40 50 60] 5
[40] 1 3
[99 -1 40] [99 -1 40]

unsafe.SliceData returns the pointer inside the header, so the second line proves that backing[1:4] points at backing[1] and copies no data. The header is 24 bytes on a 64-bit platform (it is 3 * 8), so passing a slice to a function costs three words however many elements it holds. Because the header is copied by value but the pointer inside it is shared, a function that receives a slice can modify the caller’s elements, but it cannot change the caller’s length or capacity. To let a function grow a slice, return the new slice (the append pattern) or pass a pointer to the slice.

Diagram of a slice header over a backing array before and after append exceeds capacity. Before: header with ptr, len 3 and cap 4 points at an array holding 1, 2, 3 and one spare cell. After s = append(s, 4, 5): the header has len 5 and cap 8 and points at a new array holding 1 to 5 and three spare cells, while the old four-cell array is greyed out and no longer reachable through s.

Reading the figure: while append finds room (len stays within cap) it writes into the existing array and returns a header with a longer length. When the new length would exceed the capacity, it allocates a larger array, copies the existing elements, adds the new ones, and returns a header that points at the new array. The old array is not touched and not modified any more through the new slice.

Slice expressions

a[low:high] forms a slice from an array, a pointer to an array, a string or another slice. The result has high - low elements, starts at a[low], and has capacity cap(a) - low. Both indexes are optional (a[:], a[low:], a[:high]); omitted they default to 0 and len(a). For arrays and strings the indexes must satisfy 0 <= low <= high <= len(a); for slices the upper bound is cap(a), not len(a), so a slice expression can extend a slice up to its capacity (s[:cap(s)] above exposes elements that s itself cannot reach).

Slicing a slice never copies elements: the new header shares the backing array with the old one, so writing through either is visible through both. This is the cause of the classic aliasing bugs below and the reason why slices are so cheap to pass around.

A constant index that is negative or out of range is rejected by the compiler. For non-constant indexes the checks happen at run time:

package main

import "fmt"

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s[2:1])
	fmt.Println(s[-1])
	m := map[[]int]string{}
	fmt.Println(m)
}
./main.go:7:18: invalid slice indices: 1 < 2
./main.go:8:16: invalid argument: index -1 (constant of type int) must not be negative
./main.go:9:11: invalid map key type []int
package main

import "fmt"

func main() {
	s := []int{1, 2, 3}
	n := 5
	fmt.Println(s[1:n])
}
panic: runtime error: slice bounds out of range [:5] with capacity 3

goroutine 1 [running]:
...

Slicing a string gives a string (and shares its bytes) and slicing an array requires it to be addressable. See Strings, Runes and UTF-8 for why the indexes of a string are byte offsets, not characters.

nil slices and empty slices

The zero value of a slice is nil: a header whose pointer is nil and whose length and capacity are zero. A nil slice is perfectly usable: len, cap, range and append all work on it, so most code never needs to initialize a slice before appending. The empty slice []T{} (or make([]T, 0)) is not nil, but it has the same length and capacity.

package main

import (
	"encoding/json"
	"fmt"
	"slices"
)

func main() {
	var nilSlice []string     // zero value: nil
	empty := []string{}       // non-nil, length 0
	made := make([]string, 0) // non-nil, length 0

	fmt.Println(nilSlice == nil, empty == nil, made == nil)
	fmt.Println(len(nilSlice), cap(nilSlice), len(empty), cap(empty))
	fmt.Printf("%v %v %#v %#v\n", nilSlice, empty, nilSlice, empty)

	// append, range, len and cap all work on a nil slice.
	nilSlice = append(nilSlice, "Dune")
	fmt.Println(nilSlice, nilSlice == nil)

	// nil and empty differ for encoding/json and for reflect.DeepEqual, but not for slices.Equal.
	var n []string
	a, _ := json.Marshal(n)
	b, _ := json.Marshal([]string{})
	fmt.Println(string(a), string(b))
	fmt.Println(slices.Equal(n, []string{}))
}
true false false
0 0 0 0
[] [] []string(nil) []string{}
[Dune] false
null []
true

Practical rules:

  • Test for emptiness with len(s) == 0, not s == nil, so that nil and empty slices are treated alike.

  • Declare with var s []T when you will append; there is no need for s := []T{} or make([]T, 0) unless you need the non-nil value.

  • The difference is visible in a few places. encoding/json writes a nil slice as null and an empty slice as [] (see encoding/json), reflect.DeepEqual treats them as different, and s == nil obviously does. slices.Equal compares elements only, so it treats them as equal.

  • A slice can be compared with nil and nothing else. Comparing two slices with == is a compile error (below).

package main

import "fmt"

func main() {
	a := []int{1, 2}
	b := []int{1, 2}
	fmt.Println(a == b)
}
./main.go:8:14: invalid operation: a == b (slice can only be compared to nil)

Use slices.Equal (elements compared with ==), slices.EqualFunc or reflect.DeepEqual when you need to compare the contents of two slices.

make and composite literals

make([]T, length) creates a slice of length zero values whose capacity equals the length. make([]T, length, capacity) allocates room for capacity elements and sets the length to length. make is the only way to create a slice of a size known only at run time, and the three-argument form is the way to preallocate when you know how many elements you will append. The arguments must be non-negative integers and length <= capacity; constants that violate this are compile errors, and run-time values panic.

package main

import "fmt"

func main() {
	// make([]T, len) and make([]T, len, cap)
	ids := make([]int, 3)      // len 3, cap 3, zero-filled
	buf := make([]byte, 0, 16) // len 0, cap 16: room to grow without reallocating
	fmt.Println(ids, len(ids), cap(ids))
	fmt.Println(buf, len(buf), cap(buf))

	// Composite literal
	lit := []string{"Dune", "Emma"}
	fmt.Println(lit, len(lit), cap(lit))

	// Indexed literal
	idx := []string{3: "last"}
	fmt.Printf("%q %d\n", idx, len(idx))

	// new gives a pointer to a nil slice, rarely useful
	p := new([]int)
	fmt.Println(*p == nil)

	// A slice of slices is not contiguous
	rows := make([][]int, 2)
	for i := range rows {
		rows[i] = make([]int, 3)
	}
	rows[1][2] = 7
	fmt.Println(rows)
}
[0 0 0] 3 3
[] 0 16
[Dune Emma] 2 2
["" "" "" "last"] 4
true
[[0 0 0] [0 0 7]]
package main

import "fmt"

func main() {
	s := make([]int, 5, 2)
	t := make([]int, -1)
	fmt.Println(s, t)
}
./main.go:6:19: invalid argument: length and capacity swapped
./main.go:7:19: invalid argument: index -1 (constant of type int) must not be negative

Remember the difference between the two sizes. make([]int, 10) makes a slice that already has ten elements; appending to it makes eleven. To build a slice by appending, write make([]int, 0, 10). Confusing the two is the most common make mistake, and it leaves ten zeros at the front of the result.

A composite literal such as []string{"Dune", "Emma"} allocates an array and returns a slice that covers all of it, and, like arrays, slice literals can use indexed elements ([]string{3: "last"}). new([]int) returns a pointer to a nil slice and is almost never useful.

append and capacity growth

append(s, x1, x2) returns a slice that contains the elements of s followed by the new ones. If the capacity of s is large enough, the result shares s's backing array; otherwise Go allocates a new, larger array and copies the old elements. Either way, append returns the slice you must use. Like most builtins that produce a value, it cannot be used as a statement on its own, so calling append and discarding the result is a compile error.

package main

func main() {
	s := []int{1, 2}
	append(s, 3)
}
./main.go:5:2: append(s, 3) (value of type []int) is not used

A variadic form appends another slice: append(s, t...). As a special case, append accepts a string when the destination is a []byte: append(b, "text"...). go vet reports a call with no values at all (s = append(s)), because it does nothing:

main.go:7:6: append with no values

How the capacity grows

The spec says only that a new, sufficiently large underlying array is allocated. The growth policy is a runtime implementation detail, but the current one is worth knowing because it explains why appending in a loop is cheap on average (amortized constant time per element). While a slice is small, the capacity roughly doubles; from a threshold of 256 elements on, the formula transitions smoothly to growth of about 1.25 times plus a constant, and the result is then rounded up to the size class of the memory allocator. The program prints the capacity each time it changes:

package main

import "fmt"

func main() {
	var s []int
	last := -1
	for i := range 2000 {
		s = append(s, i)
		if c := cap(s); c != last {
			fmt.Printf("len=%-5d cap=%d\n", len(s), c)
			last = c
		}
	}
}
len=1     cap=4
len=5     cap=8
len=9     cap=16
len=17    cap=32
len=33    cap=64
len=65    cap=128
len=129   cap=256
len=257   cap=512
len=513   cap=848
len=849   cap=1280
len=1281  cap=1792
len=1793  cap=2560

The doubling continues to 256 and then slows (512, 848, 1280, …​). The sequence depends on the element size and on the allocator’s size classes, so it is different for []byte or []struct{...}, and it can change between releases: never hard-code it. Go 1.18 introduced the current formula. Before it, slices doubled until 1024 elements, which caused a sudden change in allocation behavior at that point. If you know the final size, make([]T, 0, n) (or slices.Grow) avoids all intermediate allocations.

Aliasing after append

Whether append reallocates decides whether two slices still share elements. That is the most subtle behavior of slices:

package main

import "fmt"

func main() {
	a := make([]int, 3, 4)
	copy(a, []int{1, 2, 3})

	b := append(a, 4) // fits in the capacity: shares a's backing array
	c := append(a, 5) // overwrites the same slot that b[3] uses
	fmt.Println(a, b, c)

	d := append(b, 6) // exceeds the capacity: new backing array
	d[0] = 100
	fmt.Println(b, d)

	// The idiom: assign the result back to the same variable.
	var s []int
	s = append(s, 1, 2, 3)
	s = append(s, []int{4, 5}...)
	s = append(s[:1], s[2:]...) // delete index 1 (older idiom; slices.Delete is clearer)
	fmt.Println(s)

	// append can add a string to a []byte
	bs := append([]byte("shelf: "), "Dune"...)
	fmt.Println(string(bs))
}
[1 2 3] [1 2 3 5] [1 2 3 5]
[1 2 3 5] [100 2 3 5 6]
[1 3 4 5]
shelf: Dune

The first two appends fit in the capacity of a (4), so b and c both wrote element 3 of the same array: b now shows 5, not 4. The third append exceeded the capacity, so d got its own array and writing to d[0] left b untouched. The rules that follow:

  • Treat the slice you passed to append as consumed. Always write s = append(s, ...) and use only the result.

  • If a function modifies a slice’s elements, document it. A function that appends to its parameter may or may not modify the caller’s array, depending on spare capacity.

  • To deliberately avoid sharing, copy (below) or use the full slice expression to cap the capacity.

copy

copy(dst, src) copies min(len(dst), len(src)) elements from src to dst and returns the number of elements copied. It allocates nothing: the destination must already have room, and it copies into the elements the destination has (its length), not its capacity. The slices may overlap (it works like memmove), and as a special case the source may be a string when the destination is a []byte.

package main

import "fmt"

type Book struct {
	Title string
	Tags  []string
}

func main() {
	src := []int{1, 2, 3, 4, 5}
	dst := make([]int, 3)
	n := copy(dst, src) // copies min(len(dst), len(src)) elements
	fmt.Println(n, dst)

	dst2 := []int{} // length 0: copies nothing
	fmt.Println(copy(dst2, src), dst2)

	// Overlapping copy within one slice works like memmove.
	shift := []int{1, 2, 3, 4, 5}
	copy(shift[1:], shift)
	fmt.Println(shift)

	// Special case: copy from a string into []byte
	b := make([]byte, 4)
	copy(b, "Dune and more")
	fmt.Println(string(b))

	// copy is shallow: the elements are copied, anything they reference is shared.
	books := []Book{{"Dune", []string{"sf"}}}
	cp := make([]Book, len(books))
	copy(cp, books)
	cp[0].Title = "Changed"
	cp[0].Tags[0] = "classic" // the same array of tags
	fmt.Println(books, cp)
}
3 [1 2 3]
0 []
[1 1 2 3 4]
Dune
[{Dune [classic]}] [{Changed [classic]}]

Key points:

  • copy into a zero-length destination copies nothing. A common mistake is dst := []int{} or var dst []int followed by copy(dst, src); size the destination first (make([]int, len(src))) or use slices.Clone(src).

  • copy is shallow. It copies the elements, and if those are pointers, slices, maps or structs containing them, both slices refer to the same underlying data (the shared Tags array above). It never deep copies and never allocates a new backing array; there is no built-in deep copy in Go. The same is true of slices.Clone and of assigning a struct that contains a slice.

  • Overlapping copies are safe, so copy(s[1:], s) shifts the elements one place to the right, as shown.

The full slice expression

A slice expression can take a third index: a[low:high:max]. It sets the length to high - low as usual, but sets the capacity to max - low. The indexes must satisfy 0 <= low <= high <= max <= cap(a). Only the first index can be omitted (it defaults to zero) and it cannot be used with strings.

package main

import "fmt"

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

	s := arr[1:3]   // len 2, cap 5
	t := arr[1:3:3] // full slice expression: len 2, cap 2
	fmt.Println(s, len(s), cap(s))
	fmt.Println(t, len(t), cap(t))

	s = append(s, 100) // writes arr[3]
	fmt.Println(arr)

	t = append(t, 200) // capacity exhausted: copies to a new array, arr is untouched
	fmt.Println(arr, t)

	// Typical use: hand out a view that the callee cannot grow into your data.
	head := arr[:2:2]
	head = append(head, -1)
	fmt.Println(arr, head)

	// Full slice expressions also work on slices and need 0 <= low <= high <= max <= cap.
	sl := []int{1, 2, 3, 4, 5}
	fmt.Println(sl[1:2:4], cap(sl[1:2:4]))
}
[1 2] 2 5
[1 2] 2 2
[0 1 2 100 4 5]
[0 1 2 100 4 5] [1 2 200]
[0 1 2 100 4 5] [0 1 -1]
[2] 3

By limiting t to a capacity of 2 the append on it must allocate, so it can no longer overwrite arr[3]. Use the three-index form whenever you give out a sub-slice and the receiver may append to it: a sub-slice of a parser’s buffer, a window onto a pool of reusable memory, the head of a slice that you are going to split in two. It is the only way to cap the capacity without copying. s[:n:n] is the common idiom (written for a slice s and a count n).

Converting slices to arrays

Go 1.17 added the conversion from a slice to a pointer to an array, and Go 1.20 added the conversion to an array value. In both cases the slice length must be at least the array length, or the conversion panics at run time:

package main

import "fmt"

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

	arr := [4]int(s) // Go 1.20: slice to array conversion copies the elements
	arr[0] = 99
	fmt.Println(s, arr)

	p := (*[4]int)(s) // Go 1.17: slice to array pointer shares the backing array
	p[1] = 77
	fmt.Println(s, *p)

	sub := [2]int(s[2:]) // the length must be at least the array length
	fmt.Println(sub)

	defer func() {
		fmt.Println("recovered:", recover())
	}()
	_ = [5]int(s)
}
[1 2 3 4] [99 2 3 4]
[1 77 3 4] [1 77 3 4]
[3 4]
recovered: runtime error: cannot convert slice with length 4 to array or pointer to array with length 5
  • [4]int(s) makes a copy of the first four elements. Changing the array leaves the slice alone.

  • (*[4]int)(s) is a view: the pointer refers to the same elements as the slice, so writes through p modify s. The array pointer keeps the whole backing array alive.

  • A nil slice converts to a nil array pointer only for the length-zero array type; any other length panics.

  • Both conversions replaced older code that cast through unsafe. They are checked by the compiler and the runtime.

Converting the other way, from an array to a slice, is just a slice expression: arr[:], which needs arr to be addressable (a variable, not a function result).

clear

The clear builtin (Go 1.21) zeroes every element of a slice, keeping its length and capacity, and deletes every entry of a map. For a slice it is the loop for i := range s { s[i] = *new(T) } made into one operation that the compiler can turn into a fast memory clear. The main use is dropping references so the garbage collector can reclaim them:

package main

import "fmt"

type Book struct {
	Title string
	Pages int
}

func main() {
	nums := []int{1, 2, 3}
	clear(nums) // zeroes every element, keeps the length
	fmt.Println(nums, len(nums))

	ptrs := []*Book{{"Dune", 612}, {"Emma", 474}}
	clear(ptrs[1:]) // lets the garbage collector reclaim the second *Book
	fmt.Println(ptrs[0].Title, ptrs[1] == nil)

	m := map[string]int{"a": 1}
	clear(m) // for maps it deletes every entry
	fmt.Println(len(m))
}
[0 0 0] 3
Dune true
0

clear(s) does not shorten the slice; to forget the elements you also reslice (s = s[:0]). It is a no-op on nil.

The slices package

Before Go 1.21 the standard library had no generic slice helpers and the community wrote the same loops over and over; the Go wiki’s SliceTricks page collected the idioms (append(s[:i], s[i+1:]...) to delete, a splice with two append calls to insert, filtering in place, reversing). The slices package (Go 1.21) provides these operations as functions that work on slices of any element type. Prefer them: they are shorter, tested, and avoid the mistakes the hand-written versions are prone to, such as leaking the tail of the array after a delete.

package main

import (
	"cmp"
	"fmt"
	"slices"
	"strings"
)

type Book struct {
	Title  string
	Author string
	Year   int
}

func main() {
	nums := []int{5, 2, 8, 2, 9, 1}

	cp := slices.Clone(nums) // new backing array; Clone(nil) stays nil
	slices.Sort(cp)
	fmt.Println(nums, cp)

	fmt.Println(slices.Contains(nums, 8), slices.Index(nums, 2), slices.Index(nums, 42))
	fmt.Println(slices.Max(nums), slices.Min(nums))

	i, found := slices.BinarySearch(cp, 8)
	fmt.Println(i, found)

	cp = slices.Compact(cp) // removes consecutive duplicates (sort first)
	fmt.Println(cp)

	cp = slices.Insert(cp, 1, 100, 101) // insert at index 1
	fmt.Println(cp)
	cp = slices.Delete(cp, 1, 3) // delete [1:3)
	fmt.Println(cp)

	fmt.Println(slices.Concat([]int{1}, []int{2, 3}, nil, []int{4}))
	fmt.Println(slices.Repeat([]string{"ab"}, 3))

	g := slices.Grow([]int{1}, 100) // guarantees room for 100 more appends
	fmt.Println(len(g), cap(g) >= 101)

	slices.Reverse(nums)
	fmt.Println(nums, slices.Equal(nums, []int{1, 9, 2, 8, 2, 5}))

	books := []Book{
		{"Emma", "Austen", 1815},
		{"Dune", "Herbert", 1965},
		{"Persuasion", "Austen", 1817},
	}
	slices.SortFunc(books, func(a, b Book) int {
		return cmp.Or(
			strings.Compare(a.Author, b.Author),
			cmp.Compare(a.Year, b.Year),
		)
	})
	fmt.Println(books)

	idx, ok := slices.BinarySearchFunc(books, "Herbert", func(b Book, name string) int {
		return strings.Compare(b.Author, name)
	})
	fmt.Println(idx, ok)

	j := slices.IndexFunc(books, func(b Book) bool { return b.Year > 1900 })
	fmt.Println(j, slices.ContainsFunc(books, func(b Book) bool { return b.Title == "Nope" }))

	books = slices.DeleteFunc(books, func(b Book) bool { return b.Author == "Austen" })
	fmt.Println(books)

	for c := range slices.Chunk([]int{1, 2, 3, 4, 5}, 2) {
		fmt.Print(c, " ")
	}
	fmt.Println()
	fmt.Println(slices.Collect(slices.Values([]int{7, 8})), slices.Sorted(slices.Values([]int{3, 1, 2})))
}
[5 2 8 2 9 1] [1 2 2 5 8 9]
true 1 -1
9 1
4 true
[1 2 5 8 9]
[1 100 101 2 5 8 9]
[1 2 5 8 9]
[1 2 3 4]
[ab ab ab]
1 true
[1 9 2 8 2 5] true
[{Emma Austen 1815} {Persuasion Austen 1817} {Dune Herbert 1965}]
2 true
2 false
[{Dune Herbert 1965}]
[1 2] [3 4] [5]
[7 8] [1 2 3]

Source: slices package

The functions used above, in groups:

Function What it does

slices.Clone(s)

New slice with a copy of the elements (shallow); Clone(nil) returns nil

slices.Equal, slices.EqualFunc

Compare element by element

slices.Compare, slices.CompareFunc

Lexicographic comparison, returns -1, 0 or +1

slices.Index, slices.IndexFunc

Position of the first match, or -1

slices.Contains, slices.ContainsFunc

Membership test

slices.Insert(s, i, v...)

Insert values at index i; panics if i > len(s)

slices.Delete(s, i, j)

Remove s[i:j]; returns the shortened slice

slices.DeleteFunc(s, f)

Remove all elements for which f is true

slices.Replace(s, i, j, v...)

Replace s[i:j] with the given values

slices.Compact, slices.CompactFunc

Remove consecutive duplicates

slices.Concat(s...)

New slice with all the elements of the arguments (Go 1.22)

slices.Repeat(s, n)

New slice repeating s n times (Go 1.23)

slices.Grow(s, n)

Guarantee room for n more elements without reallocating

slices.Clip(s)

Reduce capacity to length (s[:len(s):len(s)])

slices.Reverse(s)

Reverse in place

slices.Sort, slices.SortFunc, slices.SortStableFunc

Sort in place; the Func forms take a three-way cmp function

slices.IsSorted, slices.IsSortedFunc

Check ordering

slices.BinarySearch, slices.BinarySearchFunc

Position in a sorted slice and whether the item was found

slices.Min, slices.Max, slices.MinFunc, slices.MaxFunc

Smallest and largest; panic on an empty slice

slices.All, slices.Values, slices.Backward

Iterators over index and value pairs, values, or in reverse (Go 1.23)

slices.Collect, slices.Sorted, slices.SortedFunc, slices.AppendSeq

Build a slice from an iterator (Go 1.23)

slices.Chunk(s, n)

Iterator over consecutive sub-slices of at most n elements (Go 1.23)

Notes about the behavior:

  • The functions that shrink a slice (Delete, DeleteFunc, Compact, CompactFunc, Replace) zero the elements between the new length and the old length since Go 1.22. That lets the garbage collector reclaim pointed-to values, but it also means that a variable that still holds the original slice sees the zeros. Always use the returned slice.

  • Sort uses the pattern-defeating quicksort introduced in Go 1.19 and is not stable. Use SortStableFunc when equal elements must keep their order. The comparison function returns a negative number, zero or a positive number, which cmp.Compare and strings.Compare produce, and cmp.Or chains several comparisons, as the example does.

  • BinarySearch requires a slice that is sorted in the same order as the comparison.

  • slices.Sorted(maps.Keys(m)) is the idiom for ranging over a map in key order; see Maps. The iterator functions are explained on Iterators and Range-over-Func.

  • The pre-1.21 sort package still exists (sort.Ints, sort.Slice) and works, but slices.Sort and slices.SortFunc are type safe, faster and shorter, so new code should use them.

Memory: retained arrays and the garbage collector

A slice keeps its whole backing array alive, however small the window it views. Two situations leak memory without any pointer bug in the code.

First, a small sub-slice of a large slice pins the large array. Parsing a one megabyte response and keeping a four-byte []byte view of its header retains one megabyte. Copy what you want to keep: slices.Clone(head) allocates a new, small array and drops the reference to the big one. Second, removing elements from the middle of a slice of pointers by shifting leaves the old pointer in the tail of the array, where the garbage collector still sees it. The older append(s[:i], s[i+1:]...) idiom has that problem; slices.Delete (since Go 1.22) and clear of the tail fix it.

package main

import (
	"fmt"
	"slices"
)

type Loan struct{ ID int }

func main() {
	loans := []*Loan{{1}, {2}, {3}, {4}}

	// Older idiom: the tail element still points at a Loan, so it stays reachable.
	old := slices.Clone(loans)
	old = append(old[:1], old[2:]...)
	fmt.Println(len(old), cap(old), old[:cap(old)][3] != nil)

	// slices.Delete zeroes the vacated tail elements (Go 1.22).
	loans = slices.Delete(loans, 1, 2)
	fmt.Println(len(loans), cap(loans), loans[:cap(loans)][3] == nil)

	// Retaining a tiny sub-slice keeps the whole backing array alive.
	big := make([]byte, 1<<20)
	big[0] = 'X'
	head := big[:4]                // len 4 but pins 1 MiB
	copyHead := slices.Clone(head) // cap is small; big can be collected
	fmt.Println(len(head), cap(head), len(copyHead), cap(copyHead))
}
3 4 true
3 4 true
4 1048576 4 8

The first line shows that the hand-written deletion leaves a non-nil pointer in the last element of the array, and the second shows that slices.Delete zeroed it. The third line shows head (length 4 but capacity one megabyte) next to the clone, whose capacity is the small allocator size class. The same applies to a struct that stores a sub-slice, or a cache that keeps a subslice of a request buffer. See Memory Management and Garbage Collection for how the collector decides what is reachable.

Slices as reusable buffers and in-place filtering

Because a slice is cheap to reset (s = s[:0] keeps the capacity), it is the usual way to reuse a buffer across loop iterations, and because append writes into spare capacity, you can filter a slice without allocating by writing the kept elements back into its own array:

package main

import (
	"fmt"
	"slices"
)

type Book struct {
	Title string
	Year  int
}

// filterInPlace reuses the backing array of books and keeps only matches.
func filterInPlace(books []Book, keep func(Book) bool) []Book {
	out := books[:0] // same array, length 0
	for _, b := range books {
		if keep(b) {
			out = append(out, b)
		}
	}
	clear(books[len(out):]) // drop stale tail values
	return out
}

func main() {
	books := []Book{{"Emma", 1815}, {"Dune", 1965}, {"Ulysses", 1922}, {"Neuromancer", 1984}}
	recent := filterInPlace(books, func(b Book) bool { return b.Year >= 1950 })
	fmt.Println(recent)
	fmt.Println(books) // the original slice sees the overwrite

	// The same with the standard library, as a new slice:
	all := []Book{{"Emma", 1815}, {"Dune", 1965}}
	kept := slices.DeleteFunc(slices.Clone(all), func(b Book) bool { return b.Year < 1950 })
	fmt.Println(kept, all)

	// A reusable buffer: reset the length, keep the capacity.
	buf := make([]byte, 0, 64)
	for _, w := range []string{"alpha", "beta"} {
		buf = buf[:0]
		buf = append(buf, w...)
		buf = append(buf, '!')
		fmt.Println(string(buf), len(buf), cap(buf))
	}
}
[{Dune 1965} {Neuromancer 1984}]
[{Dune 1965} {Neuromancer 1984} { 0} { 0}]
[{Dune 1965}] [{Emma 1815} {Dune 1965}]
alpha! 6 64
beta! 5 64

Rules for in-place filtering and reuse:

  • Writing back into books[:0] is safe because the write position never gets ahead of the read position.

  • The original slice’s length is unchanged, so its tail now holds stale (here, zeroed) elements. Clear them so pointers are not retained, as the example does, and use only the returned slice afterwards.

  • When another slice shares the array, in-place filtering corrupts what that other slice sees. Clone first (slices.Clone), as kept does in the second part of the example, when you must not disturb the original.

  • A buffer reused with buf = buf[:0] keeps its capacity forever; to return memory after a spike, allocate a new smaller buffer. For many goroutines use a sync.Pool; see Sync and Atomics.

Run-time panics

All out-of-range operations panic with a runtime.Error; they are programmer errors, not conditions to be handled. The messages tell you the index and the length (or capacity):

package main

import "fmt"

func main() {
	s := []int{1, 2, 3}
	defer func() { fmt.Println("recovered:", recover()) }()
	for i := 0; i <= 3; i++ {
		fmt.Println(s[i])
	}
}
1
2
3
recovered: runtime error: index out of range [3] with length 3
Operation Panic message pattern

s[i] with i >= len(s)

index out of range [3] with length 3

s[i] with a negative non-constant i

index out of range [-1]

s[a:b] with b > cap(s)

slice bounds out of range [:5] with capacity 3

s[a:b] with a > b

slice bounds out of range [3:2]

[4]int(s) with len(s) < 4

cannot convert slice with length 4 to array or pointer to array with length 5 (for a 5-element array)

make([]T, n) with negative n

makeslice: len out of range

slices.Insert(s, i, ...) with i > len(s)

slice bounds out of range [5:3] (for i 5 and length 3)

slices.Max(nil)

slices.Max: empty list

In for i := 0; i <= len(s); i++ the <= is the classic off-by-one that causes the first of these panics. Prefer for i := range s or for i, v := range s, which cannot go out of range. Range over an integer (for i := range n, Go 1.22) is the matching form when you need only a count. The bounds checks cost little, and the compiler removes many of them when it can prove the index is in range.

Where the books are outdated or wrong

Two of the books this reference consulted for its structure describe an older Go, and a few statements should not be carried over:

  • Bodner (Learning Go, chapter 3) describes the growth of a slice’s capacity as doubling until 1024 elements. Since Go 1.18 the threshold is 256 and the growth is smoothed, as the program above prints, so the numbers in older articles do not match a current toolchain. The chapter also predates the slices package, the clear builtin and the conversion from slices to arrays (1.17 for pointers, 1.20 for values); today slices.Clone, slices.Delete, slices.Insert and slices.Contains replace the hand-written loops and append tricks.

  • Vivien (Learning Go Programming, chapter 7) is accurate about the slice header, sharing and three-index slicing, but the description of copy as returning a deep copy with a new underlying array is wrong: copy shallowly copies min(len(dst), len(src)) elements into an existing destination and allocates nothing; slices.Clone allocates a new array and is also shallow. It does not mention the full slice expression’s role in preventing an append from overwriting shared data, array-to-slice conversions or any of the generic helpers.

  • Both books show the older sort.Ints and sort.Slice style. They still work; slices.Sort and slices.SortFunc are the current choice for new code.

Version notes

Go release Change

1.17

Conversion from a slice to an array pointer ((*[N]T)(s)); unsafe.Slice

1.18

New, smoother capacity growth formula (threshold 256)

1.19

sort switched to pattern-defeating quicksort (used later by slices.Sort)

1.20

Conversion from a slice to an array value ([N]T(s)); unsafe.SliceData

1.21

clear, min and max builtins; the slices and maps packages

1.22

slices.Concat; Delete, Compact and Replace zero the vacated tail; slices.Insert always panics on a bad index; range over integers; per-iteration loop variables

1.23

Iterator functions in slices (All, Values, Backward, Collect, Sorted, Chunk, AppendSeq) and slices.Repeat

No release after Go 1.23 has changed the language rules for arrays or slices; Go 1.27 behaves as described here.