golang · level 4

Slices & Maps

Go's two everyday containers — gotchas and idioms.

125 XP

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.