
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 need | Use |
|---|---|
| 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.
Learn more: