Arrays and Slices

Updated

September 13, 2026

Arrays and Slices

An array is a value with a fixed length in its type. A slice is a view: a pointer, a length, and a capacity. The boring default is slices everywhere, make when you know the length, append when you do not, and copy when the new slice must not share memory.

Mental model

[3]int and [4]int are different types. Assigning an array copies every element.

A slice header is small. Two slices can point at the same backing array. A change through one is visible through the other. append writes into leftover capacity when it can; only when capacity is full does it allocate a new array.

len is how many elements you may index. cap is how far append can go before it reallocates.

Worked examples

Case 1: Arrays copy, slices share

Save as array_copy.go. The array keeps its old first slot. The slice does not.

// array_copy.go
package main

import "fmt"

func main() {
    var seats [3]int
    seats[0] = 2
    seats[1] = 4
    seats[2] = 2
    other := seats
    other[0] = 8
    fmt.Println("array", seats, other)

    open := []int{2, 4, 2}
    alias := open
    alias[0] = 8
    fmt.Println("slice", open, alias)
}

Run:

go run array_copy.go

Output:

array [2 4 2] [8 4 2]
slice [8 4 2] [8 4 2]

You will almost never put an array in desk code. You will put slices in desk code, and you will forget they share.

Case 2: make, len, cap

Save as make_slice.go. Length 0, capacity 2: room for two appends before growth.

// make_slice.go
package main

import "fmt"

func main() {
    items := make([]string, 0, 2)
    fmt.Printf("len=%d cap=%d %v\n", len(items), cap(items), items)
    items = append(items, "toast")
    fmt.Printf("len=%d cap=%d %v\n", len(items), cap(items), items)
    items = append(items, "tea")
    fmt.Printf("len=%d cap=%d %v\n", len(items), cap(items), items)
    items = append(items, "soup")
    fmt.Printf("len=%d cap=%d %v\n", len(items), cap(items), items)
}

Run:

go run make_slice.go

Output:

len=0 cap=2 []
len=1 cap=2 [toast]
len=2 cap=2 [toast tea]
len=3 cap=4 [toast tea soup]

The third append needed a new backing array. Capacity doubled here. Do not hard-code growth numbers; they are an implementation detail. Do depend on len.

Case 3: Slicing is a window, not a copy

Save as window.go. tickets[1:3] is length 2. Index 0 of the window is the original index 1.

// window.go
package main

import "fmt"

func main() {
    tickets := []int{10, 20, 30, 40}
    mid := tickets[1:3]
    fmt.Println("mid", mid, "len", len(mid), "cap", cap(mid))
    fmt.Println("mid[0]", mid[0])
}

Run:

go run window.go

Output:

mid [20 30] len 2 cap 3
mid[0] 20

cap is 3 because the backing array still has 40 after the window. mid[2] panics (len is 2), but append(mid, 99) would write into that leftover slot.

Case 4: copy owns its own array

Save as copy_tickets.go. copy returns how many elements moved: the min of the two lengths.

// copy_tickets.go
package main

import "fmt"

func main() {
    tickets := []int{10, 20, 30}
    out := make([]int, len(tickets))
    n := copy(out, tickets)
    out[0] = 99
    fmt.Println("copied", n)
    fmt.Println("orig", tickets)
    fmt.Println("out", out)
}

Run:

go run copy_tickets.go

Output:

copied 3
orig [10 20 30]
out [99 20 30]

append([]int(nil), tickets...) also copies. Use whichever you can read at speed.

Case 5: The 3-index slice expression (s[low:high:max])

Save as full_slice.go. Slicing normally keeps the capacity of the original array up to its end. A full slice expression adds a third index: s[low:high:max]. This sets capacity explicitly to max - low.

// full_slice.go
package main

import "fmt"

func main() {
    tickets := []int{10, 20, 30, 40}

    // 3-index slice: tickets[low:high:max]
    // low=0, high=2, max=2 -> len=2, cap=2
    window := tickets[0:2:2]
    fmt.Printf("window: len=%d cap=%d\n", len(window), cap(window))

    // Because cap is reached, append MUST allocate a new backing array!
    window = append(window, 99)
    fmt.Println("window after append:", window)
    fmt.Println("original tickets untouched:", tickets)
}

Run:

go run full_slice.go

Output:

window: len=2 cap=2
window after append: [10 20 99]
original tickets untouched: [10 20 30 40]

By clamping capacity to length with tickets[0:2:2], any subsequent append is forced to allocate a fresh backing array. You protect tickets[2] from accidental overwrite without copying eagerly.

Case 6: Deleting elements with slices.Delete

Save as delete_slice.go. Go does not have a built-in delete keyword for slices. The standard library provides slices.Delete(s, i, j), which shifts trailing elements left and zeroes out the discarded slots.

// delete_slice.go
package main

import (
    "fmt"
    "slices"
)

func main() {
    orders := []string{"toast", "tea", "soup", "coffee"}

    // Delete index 1 ("tea"): removes elements in orders[1:2]
    orders = slices.Delete(orders, 1, 2)
    fmt.Printf("after delete: %v (len=%d cap=%d)\n", orders, len(orders), cap(orders))
}

Run:

go run delete_slice.go

Output:

after delete: [toast soup coffee] (len=3 cap=4)

slices.Delete zeroes out the vacated elements at the end of the slice before truncating len, ensuring pointers or references do not linger in memory.

The trap

A subslice plus append can overwrite the original. This is the slice bug that survives code review. Save as alias_append.go:

// alias_append.go
package main

import "fmt"

func main() {
    tickets := []int{10, 20, 30, 40}
    window := tickets[0:2]
    window[0] = 99
    fmt.Println("after index", tickets, window)

    window = append(window, 70)
    fmt.Println("after append", tickets, window)
}

Run:

go run alias_append.go

Output:

after index [99 20 30 40] [99 20]
after append [99 20 70 40] [99 20 70]

window had capacity left, so append wrote 70 into tickets[2]. The kitchen ticket 30 is gone.

The fix is a copy before you append, append onto nil, or the 3-index slice expression from Case 5:

// alias_copy.go
package main

import "fmt"

func main() {
    tickets := []int{10, 20, 30, 40}
    window := append([]int(nil), tickets[0:2]...)
    window = append(window, 70)
    fmt.Println("tickets", tickets)
    fmt.Println("window", window)
}

Run:

go run alias_copy.go

Output:

tickets [10 20 30 40]
window [10 20 70]

tickets[2] is still 30.

The boring rule

  • Use slices. Use arrays only when the length is the type (a hash, a pixel).
  • make([]T, n) when you will fill 0..n-1. make([]T, 0, n) when you will append up to n.
  • Always keep the result of append: s = append(s, v).
  • A slice expression shares. copy, append onto nil, or a 3-index slice s[i:j:j] when passing sub-slices downstream.
  • Use slices.Delete to remove elements safely without leaking trailing pointer references.
  • Never append to a subslice you still consider a window onto the original.
  • len is the contract. cap is a hint.

Try this

  1. In array_copy.go, pass the array to a function that sets seats[0] = 0. Print after the call: the caller’s array is unchanged (the parameter was a copy).
  2. In full_slice.go, change tickets[0:2:2] to tickets[0:2:4] and observe that append overwrites tickets[2].
  3. In delete_slice.go, delete the first element (orders[0:1]) and print orders.
  4. In make_slice.go, start with make([]string, 2) (length 2, zeros) and assign items[0] = "toast" instead of append.
  5. In alias_append.go, replace tickets[0:2] with tickets[0:2:2] and run again. Notice that tickets[2] remains untouched.