Back to blog

Sunday, August 16, 2026

Go Slice vs Array: When to Use Which

Title card for 'Go Slice vs Array: When to Use Which' with a short code example

Default to a slice. Reach for an array when the length is a compile-time fact you want in the type ([32]byte for a SHA-256 digest), when assignment must copy every element, or when you need ==. An array is a value of type [n]T. A slice is a header (ptr, len, cap) that points at an array. [3]int and [5]int do not mix. Every []int does.

The failure

This looks like a flexible list. It will not compile the moment the length changes.

package main

func sum(nums [3]int) int {
    n := 0
    for _, v := range nums {
        n += v
    }
    return n
}

func main() {
    a := [3]int{1, 2, 3}
    b := [4]int{1, 2, 3, 4}
    _ = sum(a)
    _ = sum(b) // cannot use b (variable of type [4]int) as [3]int value
}

The length is part of the type. You cannot write one function that accepts "an array of ints of whatever size." That is what slices are for:

func sum(nums []int) int {
    n := 0
    for _, v := range nums {
        n += v
    }
    return n
}

sum(a[:]) and sum(b[:]) both work. The [:] is a slice of the whole array.

The other common mix-up is thinking a slice assignment copies the data:

arr := [3]int{1, 2, 3}
arr2 := arr
arr2[0] = 9
// arr is still [1 2 3]

s := []int{1, 2, 3}
s2 := s
s2[0] = 9
// s is [9 2 3]: same backing array

Why they differ

An array is the elements. Assigning or passing it copies the whole block. Two [3]int values with the same elements compare equal with ==.

A slice describes elements. Assigning it copies 24 bytes on a 64-bit machine (pointer, length, capacity). The elements stay where they are. Slices are not comparable with == (only with nil).

That is why almost every Go API takes []byte and []T, not [n]T. Callers have different lengths. Copying a megabyte array into every function would be a bug you paid for in CPU.

Arrays still exist because slices need a place to put data, and because some sizes are the identity of the value: crypto/sha256 returns [Size]byte. A [16]byte IPv6 address is not "a slice that happens to be 16 long."

The fix

Use a slice unless you have a reason not to.

You needUse
Length known only at runtime, or it grows[]T + append / make
Function argument, JSON, most structs[]T
In-place mutation visible to the caller[]T (header copy, shared array)
Isolation on assignment, ==, tiny fixed buffer[n]T
Size is the type (hash, UUID, RGB)[n]T
// Slice: the normal collection.
ids := []string{"a", "b"}
ids = append(ids, "c")

// Array: the length is the meaning.
var digest [32]byte
copy(digest[:], hashed)

// Bridge: array to slice without copying elements.
sum(digest[:])

Pass []T in public APIs. Keep [n]T in tight internals, as struct fields whose size never changes, or when you want the compiler to reject the wrong length.