Skip to main content
Go · lesson 9 of 25
Go beginner Lesson 9 of 25

Arrays and Slices in Go

Go arrays and slices run for real: value vs view semantics, len and cap growth, append aliasing bugs, copy, deleting, 2D slices, and the slices package.

By SkillByExample · Updated

Arrays have a fixed size and copy like any value. Slices — what Go code uses almost everywhere — are views onto an array, and most slice surprises come from forgetting that two slices can share one array. Each program below shows one of those surprises.

Arrays are values

The length is part of an array’s type, and assigning an array copies all its elements.

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a // full copy
	b[0] = 99
	fmt.Println(a, b, a == [3]int{1, 2, 3})

	days := [...]string{"Mon", "Tue", "Wed"} // length counted for you
	fmt.Println(len(days), days)
	fmt.Printf("%T\n", days)
}
$ go run arrays.go
[1 2 3] [99 2 3] true
3 [Mon Tue Wed]
[3]string

Changing b left a untouched. Arrays of comparable elements can be compared with ==, and [...] lets the compiler count the elements. Because the size is in the type, a function taking [3]string can’t accept a [4]string — which is why you’ll rarely pass arrays around.

Slices share their backing array

A slice is a header with a pointer, a length and a capacity. Slicing or assigning copies the header, not the elements.

package main

import "fmt"

func main() {
	arr := [5]int{10, 20, 30, 40, 50}
	s := arr[1:4] // elements 1, 2, 3
	fmt.Println(s, len(s), cap(s))

	s[0] = 999
	fmt.Println(arr)

	t := s
	t[1] = -1
	fmt.Println(s, arr)
}
$ go run share.go
[20 30 40] 3 4
[10 999 30 40 50]
[999 -1 40] [10 999 -1 40 50]

arr[1:4] has length 3 (index 4 excluded) but capacity 4: it can see through to the end of arr. Writing through s changed arr, and t := s gave a second name for the same elements. This sharing is what makes slicing free — no copying — and it’s also the source of every bug in this lesson.

make, len and cap

make([]T, len, cap) allocates a backing array. append grows the slice, allocating a bigger array when capacity runs out.

package main

import "fmt"

func main() {
	var s []int
	prevCap := -1
	for i := range 2000 {
		s = append(s, i)
		if cap(s) != prevCap {
			fmt.Printf("len=%-4d cap=%d\n", len(s), cap(s))
			prevCap = cap(s)
		}
	}

	pre := make([]int, 0, 2000)
	fmt.Println(len(pre), cap(pre))
}
$ go run growth.go
len=1    cap=1
len=2    cap=2
len=3    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
0 2000

Capacity doubles up to 512; past 256 elements the growth factor eases from 2× toward 1.25×, and the result is rounded up to the allocator’s size classes — hence 512 → 848 rather than 1024. These exact numbers are a runtime detail that has changed between Go releases; the shape is what to remember. Every growth copies all existing elements to the new array. If you know the final size, make([]int, 0, n) allocates once.

The append aliasing trap

When there is spare capacity, append writes into the existing array — which another slice may be using.

package main

import "fmt"

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

	a := append(base, 4)
	b := append(base, 5)
	fmt.Println(a, b)

	c := append(base[:3:3], 6) // cap limited to 3: append must copy
	fmt.Println(base, c)
}
$ go run alias.go
[1 2 3 5] [1 2 3 5]
[1 2 3] [1 2 3 6]

a should end in 4, but it prints 5. base had room for 10, so both append calls wrote element 3 of the same array; b’s write overwrote a’s. The full slice expression base[:3:3] sets capacity equal to length, forcing append to allocate a fresh array and leaving base alone. Functions that append to a slice they were given, and callers that keep using the original, hit exactly this.

copy and Clone

copy(dst, src) copies min(len(dst), len(src)) elements and returns how many. slices.Clone makes an independent copy in one call.

package main

import (
	"fmt"
	"slices"
)

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

	dst := make([]int, 3)
	n := copy(dst, src)
	fmt.Println(n, dst)

	var empty []int
	fmt.Println(copy(empty, src), empty)

	clone := slices.Clone(src)
	clone[0] = 100
	fmt.Println(src[0], clone[0])
}
$ go run copy.go
3 [1 2 3]
0 []
1 100

copy never grows the destination: copying into a nil slice copies nothing. Size the destination with make first, or use slices.Clone.

Deleting and inserting

slices.Delete and slices.Insert (Go 1.21+) shift elements in place.

package main

import (
	"fmt"
	"slices"
)

func main() {
	s := []string{"a", "b", "c", "d", "e"}
	orig := s

	s = slices.Delete(s, 1, 3) // remove indexes 1 and 2
	fmt.Println(s, len(s))
	fmt.Printf("%q\n", orig)

	s = slices.Insert(s, 1, "X")
	fmt.Println(s)
}
$ go run delete.go
[a d e] 3
["a" "d" "e" "" ""]
[a X d e]

Delete moved d and e down over b and c in the original array, then zeroed the vacated tail — so orig, which still has length 5, now shows "" "" at the end. Deleting from a slice that other code also holds changes what that code sees.

2D slices

A slice of slices: allocate the outer slice, then each row.

package main

import "fmt"

func main() {
	rows, cols := 3, 4
	grid := make([][]int, rows)
	for i := range grid {
		grid[i] = make([]int, cols)
	}
	grid[1][2] = 7

	for _, row := range grid {
		fmt.Println(row)
	}

	jagged := [][]int{{1}, {2, 3}, {4, 5, 6}}
	fmt.Println(len(jagged[0]), len(jagged[2]))
}
$ go run grid.go
[0 0 0 0]
[0 0 7 0]
[0 0 0 0]
1 3

Each row is an independent slice, so rows can have different lengths (jagged). Forgetting the inner make leaves every row nil, and grid[1][2] = 7 would panic with index out of range.

Sorting and searching

The slices package sorts, searches and compares slices of any ordered type.

package main

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

type Person struct {
	Name string
	Age  int
}

func main() {
	nums := []int{5, 2, 8, 1, 9}
	slices.Sort(nums)
	i, found := slices.BinarySearch(nums, 8)
	fmt.Println(nums, i, found, slices.Contains(nums, 3), slices.Max(nums))

	people := []Person{{"Ann", 30}, {"Bob", 25}, {"Cy", 30}, {"Di", 25}}
	slices.SortStableFunc(people, func(a, b Person) int {
		return cmp.Compare(a.Age, b.Age)
	})
	fmt.Println(people)
}
$ go run sorting.go
[1 2 5 8 9] 3 true false 9
[{Bob 25} {Di 25} {Ann 30} {Cy 30}]

BinarySearch requires a sorted slice and returns the index plus whether the value was found. SortStableFunc takes a comparison returning negative, zero or positive (cmp.Compare produces exactly that) and keeps equal elements in their original order — Bob before Di, Ann before Cy.

Practice

1. Write appendSafe(s []int, v int) []int that never modifies the caller’s backing array, and show that alias.go’s a and b stay independent with it.

Solution
package main

import (
	"fmt"
	"slices"
)

func appendSafe(s []int, v int) []int {
	return append(slices.Clip(s), v)
}

func main() {
	base := make([]int, 3, 10)
	copy(base, []int{1, 2, 3})
	a := appendSafe(base, 4)
	b := appendSafe(base, 5)
	fmt.Println(a, b)
}
$ go run p1.go
[1 2 3 4] [1 2 3 5]

slices.Clip(s) is s[:len(s):len(s)]: with no spare capacity, every append allocates a new array.

2. Remove every even number from []int{1, 2, 3, 4, 5, 6} in place using slices.DeleteFunc.

Solution
package main

import (
	"fmt"
	"slices"
)

func main() {
	s := []int{1, 2, 3, 4, 5, 6}
	s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 })
	fmt.Println(s, len(s), cap(s))
}
$ go run p2.go
[1 3 5] 3 6

The length drops to 3, but the capacity stays 6: no new array was allocated.

3. Write chunk(s []int, size int) [][]int that splits a slice into pieces of at most size elements. Test it with 7 elements and size 3.

Solution
package main

import "fmt"

func chunk(s []int, size int) [][]int {
	var out [][]int
	for size < len(s) {
		out = append(out, s[:size:size])
		s = s[size:]
	}
	return append(out, s)
}

func main() {
	fmt.Println(chunk([]int{1, 2, 3, 4, 5, 6, 7}, 3))
}
$ go run p3.go
[[1 2 3] [4 5 6] [7]]

The chunks share the input’s array; the three-index slice s[:size:size] stops a later append to one chunk from overwriting the next. Go 1.23’s slices.Chunk returns the same pieces as an iterator.

4. Sort []string{"banana", "Apple", "cherry", "apple"} case-insensitively, and break ties so that the uppercase spelling comes first.

Solution
package main

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

func main() {
	words := []string{"banana", "Apple", "cherry", "apple"}
	slices.SortFunc(words, func(a, b string) int {
		return cmp.Or(
			cmp.Compare(strings.ToLower(a), strings.ToLower(b)),
			cmp.Compare(a, b),
		)
	})
	fmt.Println(words)
}
$ go run p4.go
[Apple apple banana cherry]

cmp.Or returns the first non-zero comparison, so the case-sensitive compare only decides ties — and "A" < "a" in byte order.

Frequently Asked Questions

What is the difference between an array and a slice in Go?
An array has a fixed length that is part of its type, so [3]int and [4]int are different types, and assigning or passing one copies every element. A slice is a small header pointing into an array, with a length and capacity; copying a slice shares the same elements.
Why must I write s = append(s, x)?
append returns a slice header that may point to a new, larger backing array when the old one is full. The original variable still describes the old array and old length, so you must store the result, normally back into the same variable.
What is the difference between len and cap of a slice?
len is how many elements the slice currently shows. cap is how many elements fit in the backing array from the slice's start before append must allocate a new array. make([]int, 0, 10) gives a slice with length 0 and room for 10.
How do I copy a slice so changes don't affect the original?
Use slices.Clone(s), or make a new slice of the same length and call copy(dst, src). Assigning t := s or re-slicing s[1:3] copies only the header, so both names still read and write the same elements.
How do I remove an element from a slice in Go?
Use slices.Delete(s, i, i+1), available since Go 1.21, which shifts later elements down and returns the shorter slice. The idiom append(s[:i], s[i+1:]...) does the same. Both modify the original backing array, so clone first if other code still uses it.