Slices & Maps
Go's two everyday containers — gotchas and idioms.
Slices & Maps
Go's two everyday containers. Together they handle most "ordered list" and "key-value lookup" needs.
Arrays vs slices
An array has a fixed compile-time size. You almost never want one directly:
var arr [5]int // fixed-size array of 5 ints
arr[0] = 10
len(arr) // 5 — and always will be
A slice is a dynamic view over an underlying array. The everyday container:
xs := []int{10, 20, 30} // slice literal
xs = append(xs, 40) // append returns the new slice
xs[0] // 10
len(xs) // 4
cap(xs) // 4 or more (capacity ≥ length)
append may grow the underlying array; you must assign the result back to capture any new backing array allocation.
Slice creation
var s1 []int // nil slice — len 0, cap 0
s2 := []int{} // empty slice — len 0, cap 0
s3 := make([]int, 5) // len 5, cap 5, zeroed: [0 0 0 0 0]
s4 := make([]int, 0, 100) // len 0, cap 100 — pre-allocate
A nil slice and an empty slice behave the same for read operations and range. Use make with a capacity when you know roughly how big the slice will grow — avoids reallocation.
Slicing
xs := []int{0, 1, 2, 3, 4, 5}
xs[1:4] // [1, 2, 3] — start..stop (exclusive)
xs[:3] // [0, 1, 2] — from start
xs[3:] // [3, 4, 5] — to end
xs[:] // full copy of the header
A slice is a view, not a copy. Modifying xs[1:4] modifies the original xs:
ys := xs[1:4]
ys[0] = 99
xs // [0, 99, 2, 3, 4, 5]
When you need an independent copy, use copy:
ys := make([]int, len(xs))
copy(ys, xs) // independent
Maps
counts := map[string]int{}
counts["alice"] = 1
counts["alice"]++ // 2
counts["bob"] // 0 — zero value for int
// "Comma ok" idiom — distinguish "absent" from "value is zero".
val, ok := counts["missing"]
if !ok {
fmt.Println("not in map")
}
// Iteration is RANDOM — every range yields keys in a different order.
for k, v := range counts {
fmt.Println(k, v)
}
delete(counts, "alice")
Maps grow as you insert. Like slices, you can pre-size with make(map[K]V, hint).
Maps vs structs
For fixed schemas use a struct, not a map. The compiler will catch typos:
// Bad: typo silently produces "" / 0.
m := map[string]string{"name": "alice"}
m["nmae"] // "" — no error
// Good: typo is a compile error.
type User struct {
Name string
}
u := User{Name: "alice"}
u.Nmae // compile error: u.Nmae undefined
append gotcha
append returns a new slice. If you forget to capture it, your append vanished:
xs := []int{1, 2, 3}
append(xs, 4) // result discarded — xs is unchanged
xs // [1, 2, 3] — silent bug
Always: xs = append(xs, 4).
Sharing across goroutines
Maps are NOT safe for concurrent use. Two goroutines writing to the same map will panic with "concurrent map writes". Use sync.RWMutex to guard access, or sync.Map for read-heavy concurrent maps.
When to reach for which
| Need | Use |
|---|---|
| Ordered, indexable, growable | slice |
| Lookup by key | map |
| Fixed schema with named fields | struct |
| Set-like semantics (membership, dedup) | map[T]struct{} (zero-byte values) |
map[string]struct{} is the idiomatic Go set: a map whose values take no space.