Arrays vs Slices in Go

Array: fixed, value type

arr := [3]int{10, 20, 30}  // size is part of the type
  • Fixed size, decided at compile time. [3]int and [4]int are different types.
  • Value semantics when you assign or pass to a function, the whole array is copied.
  • Rarely used directly in production Go code.

Slice: dynamic, reference type

s := []int{10, 20, 30}     // no size = slice
  • Dynamic size can grow/shrink.
  • Reference semantics a slice is a 3-word header: (pointer, length, capacity). Passing a slice does not copy the underlying data.
  • This is what you'll use 95% of the time.

The mental model for slices

Underlying Array:  [ 10 | 20 | 30 | 40 | 50 ]
                     ↑
Slice header:      ptr=&arr[0]  len=3  cap=5

s[0]=10, s[1]=20, s[2]=30   // only sees first 3

A slice is just a window into an underlying array.


Iteration: both are ordered

arr := [3]int{10, 20, 30}
for i, v := range arr {
    fmt.Println(i, v)   // 0 10 → 1 20 → 2 30, guaranteed
}

s := []int{10, 20, 30}
for i, v := range s {
    fmt.Println(i, v)   // same, guaranteed in insertion order
}

Both arrays and slices iterate in index order, always. No surprises there unlike maps (which are deliberately randomized in Go).


That one gotcha!

a := []int{1, 2, 3}  // Slice
b := a              // b points to the SAME underlying array

b[0] = 99
fmt.Println(a[0])   // 99 !! a is affected

This bites everyone. Because b := a copies the header, not the data. To get a true copy:

b := make([]int, len(a))
copy(b, a)

Mental Map

ArraySlice
SizeFixed at compile timeDynamic
Type[3]int[]int
AssignmentFull copy (independent data)Header copy (shared data)
Iteration orderGuaranteedGuaranteed
Use in practiceRareAlmost always

The key insight: slices don't own data, they borrow a view into an array. Everything else flows from that.

Read more