Arrays and Slices in Go
Understand Go arrays and slices, use make, append, and copy correctly, work with 2D slices, and learn slice internals.
Arrays
Arrays in Go have a fixed size that is part of their type — [5]int and [6]int are completely different types and cannot be used interchangeably. Arrays are value types, meaning assignment and function passing copies every element. This makes them predictable but expensive for large data, which is why slices (which are thin references to arrays) are used in almost all practical Go code. Arrays are primarily useful as the backing storage for slices and for small, fixed-size data like SHA256 checksums.
// Array declaration — zero-initialized automatically
var a [5]int
fmt.Println(a) // [0 0 0 0 0]
// Array literal with explicit values
primes := [5]int{2, 3, 5, 7, 11}
fmt.Println(primes[0]) // 2
fmt.Println(len(primes)) // 5
// Let the compiler count the length from the literal
days := [...]string{"Mon", "Tue", "Wed", "Thu", "Fri"}
fmt.Println(len(days)) // 5
// Arrays are comparable if their element type is comparable
a1 := [3]int{1, 2, 3}
a2 := [3]int{1, 2, 3}
fmt.Println(a1 == a2) // true
// Arrays are value types — assignment copies all elements
b := a1
b[0] = 99
fmt.Println(a1[0], b[0]) // 1, 99 — a1 is unchanged
Arrays are rarely used directly. Slices are the standard tool.
Slices
A slice is a lightweight descriptor pointing at a section of an underlying array. Because it is a reference (not a copy), passing a slice to a function is cheap regardless of how many elements it covers. Modifying a slice’s elements modifies the underlying array, which means slices that share an array see each other’s changes.
// Slice literal — Go creates an array and returns a slice covering it
fruits := []string{"apple", "banana", "cherry"}
fmt.Println(fruits) // [apple banana cherry]
fmt.Println(len(fruits)) // 3
fmt.Println(cap(fruits)) // 3
// Slice from an array — shares the array's memory
arr := [6]int{0, 1, 2, 3, 4, 5}
s := arr[1:4] // elements at index 1, 2, 3
fmt.Println(s) // [1 2 3]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5 — from index 1 to end of array
// Slices share the underlying array — modifying one affects the other
s[0] = 99
fmt.Println(arr) // [0 99 2 3 4 5] — arr is modified through the slice
Slice internals
Understanding the three-field slice header explains why append can return a different slice, why slices share memory, and why you must always use the returned slice from append.
arr: [0][1][2][3][4][5]
^ ^
| |
s.ptr end of cap
s.len = 3
s.cap = 5
A slice header contains three fields:
ptr— pointer to the first element in the underlying arraylen— number of elements currently accessiblecap— number of elements available from ptr to end of the array
make — Creating Slices with Capacity
make creates a slice with a specified length and optional capacity. Pre-allocating capacity is an important optimization: when you know roughly how many elements you will append, allocating that capacity upfront prevents append from repeatedly reallocating and copying the backing array as it grows.
// make([]Type, len) — creates a slice with len elements, all zero-valued
s := make([]int, 5)
fmt.Println(s) // [0 0 0 0 0]
// make([]Type, len, cap) — length 0 but room for 10 elements without reallocation
s2 := make([]int, 0, 10)
fmt.Println(len(s2), cap(s2)) // 0, 10
Pre-allocating capacity avoids repeated reallocations when you know the approximate final size:
func processItems(items []string) []string {
// Pre-allocate to avoid growing the backing array on every append
result := make([]string, 0, len(items))
for _, item := range items {
if len(item) > 3 {
result = append(result, item)
}
}
return result
}
append
append adds elements to a slice and returns the result. The return value is critical: if the current capacity is sufficient, append returns a slice pointing to the same array. If not, it allocates a new, larger array, copies the data, and returns a slice pointing to the new array. Always reassign the result — never assume append operated in place.
s := []int{1, 2, 3}
s = append(s, 4) // append one element
s = append(s, 5, 6, 7) // append multiple elements at once
other := []int{8, 9, 10}
s = append(s, other...) // append an entire slice with the spread operator
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]
When capacity is exceeded, Go allocates a new, larger backing array (roughly doubles capacity), copies the old data, and returns a slice pointing to the new array:
s := make([]int, 0, 3)
for i := 0; i < 7; i++ {
s = append(s, i)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
// len=1 cap=3 [0]
// len=2 cap=3 [0 1]
// len=3 cap=3 [0 1 2]
// len=4 cap=6 [0 1 2 3] ← capacity doubled: new array allocated
// len=5 cap=6 [0 1 2 3 4]
// len=6 cap=6 [0 1 2 3 4 5]
// len=7 cap=12 [0 1 2 3 4 5 6] ← capacity doubled again
copy
copy copies elements from one slice to another. It does not grow the destination — the destination must already have the required length. copy returns the number of elements actually copied, which is the minimum of the two slice lengths. Use copy whenever you need a truly independent copy of a slice that will not be affected by changes to the original.
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(n, dst) // 5 [1 2 3 4 5]
// Copy only as many elements as the destination can hold
dst2 := make([]int, 3)
copy(dst2, src)
fmt.Println(dst2) // [1 2 3]
// Two idiomatic ways to clone a slice
clone := append([]int{}, src...) // append to an empty slice
clone2 := make([]int, len(src))
copy(clone2, src) // copy into a new slice
Deleting Elements
Go has no built-in delete for slices. The idiomatic approach uses append to stitch together the parts before and after the element you want to remove. The order-preserving version is simple but shifts elements; the fast version avoids shifting by swapping the target with the last element, which is fine when order does not matter.
s := []int{1, 2, 3, 4, 5}
// Delete element at index 2 — preserves order
i := 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]
// Delete without preserving order — faster, avoids shifting all subsequent elements
s[i] = s[len(s)-1]
s = s[:len(s)-1]
2D Slices
A 2D slice is a slice of slices — each row is an independent slice, which means rows can have different lengths. This differs from a true 2D array in languages like C. The manual construction is necessary because Go does not have multi-dimensional slice literals.
// Create a 2D slice with explicit row allocation
rows, cols := 3, 4
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols) // each row is a separate slice
}
// Fill with values
for i := range matrix {
for j := range matrix[i] {
matrix[i][j] = i*cols + j
}
}
for _, row := range matrix {
fmt.Println(row)
}
// [0 1 2 3]
// [4 5 6 7]
// [8 9 10 11]
// 2D literal — useful for small, fixed boards or tables
board := [][]string{
{"_", "_", "_"},
{"_", "X", "_"},
{"_", "_", "O"},
}
board[0][0] = "X"
fmt.Println(board[1][1]) // X
Slice Tricks
These patterns come up repeatedly in Go code. Knowing them saves you from reaching for a utility library for simple slice operations.
s := []int{1, 2, 3, 4, 5}
// Reverse in place — swap elements from both ends toward the middle
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
fmt.Println(s) // [5 4 3 2 1]
// Filter without extra allocation — reuse the same backing array
filtered := s[:0] // zero-length slice sharing the same array
for _, v := range s {
if v%2 == 0 {
filtered = append(filtered, v)
}
}
// Note: filtered shares memory with s — use copy if you need independence
// Stack: append to push, slice to pop
stack := []int{}
stack = append(stack, 1, 2, 3) // push
top := stack[len(stack)-1] // peek at top
stack = stack[:len(stack)-1] // pop
// Queue: append to enqueue, slice to dequeue
// (for production use, prefer a channel or container/ring to avoid memory leaks)
queue := []int{}
queue = append(queue, 1, 2, 3) // enqueue
front := queue[0] // peek at front
queue = queue[1:] // dequeue — the front element is removed
_ = front
Sorting Slices
The sort package handles the most common sorting needs with type-specific functions for int and string slices, plus a general sort.Slice for custom comparisons. All sort functions sort in place. For searching, sort.Find provides binary search on already-sorted slices.
import "sort"
nums := []int{5, 2, 8, 1, 9, 3}
sort.Ints(nums)
fmt.Println(nums) // [1 2 3 5 8 9]
words := []string{"banana", "apple", "cherry"}
sort.Strings(words)
fmt.Println(words) // [apple banana cherry]
// Custom sort — provide a less function that returns true when i should come before j
type Person struct {
Name string
Age int
}
people := []Person{
{"Charlie", 30},
{"Alice", 25},
{"Bob", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // sort by age ascending
})
fmt.Println(people) // [{Alice 25} {Charlie 30} {Bob 35}]
// Check if sorted — useful as a precondition before binary search
fmt.Println(sort.IntsAreSorted(nums)) // true
// Binary search — slice must be sorted; returns index and whether it was found
idx, found := sort.Find(len(nums), func(i int) int {
return nums[i] - 8 // 0 means found, negative means search left, positive means search right
})
fmt.Println(idx, found) // 5, true