Arrays and Slices
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 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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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 fill0..n-1.make([]T, 0, n)when you willappendup ton.- Always keep the result of
append:s = append(s, v). - A slice expression shares.
copy,appendonto nil, or a 3-index slices[i:j:j]when passing sub-slices downstream. - Use
slices.Deleteto remove elements safely without leaking trailing pointer references. - Never
appendto a subslice you still consider a window onto the original. lenis the contract.capis a hint.
Try this
- In
array_copy.go, pass the array to a function that setsseats[0] = 0. Print after the call: the caller’s array is unchanged (the parameter was a copy). - In
full_slice.go, changetickets[0:2:2]totickets[0:2:4]and observe thatappendoverwritestickets[2]. - In
delete_slice.go, delete the first element (orders[0:1]) and printorders. - In
make_slice.go, start withmake([]string, 2)(length 2, zeros) and assignitems[0] = "toast"instead ofappend. - In
alias_append.go, replacetickets[0:2]withtickets[0:2:2]and run again. Notice thattickets[2]remains untouched.