Data Structures & Algorithms' own domain covered arrays and hash maps conceptually. Go's actual built-in implementations of "a growable list" and "a key-value map" have specific, concrete behaviors worth understanding directly — especially one classic gotcha around slices that catches even experienced Go developers.
3 min read
var arr [5]int // a fixed-size array — size is part of its TYPE
s := []int{1, 2, 3} // a slice — dynamically resizable, used almost everywhereGo has true fixed-size arrays ([5]int), but they're rarely used directly in real code — a slice ([]int, no size in the brackets) is a dynamically-sized, growable view over an underlying array, and it's what idiomatic Go code reaches for by default, conceptually closer to a Python list or a JavaScript array.
s := make([]int, 3, 5) // length 3, capacity 5
// s is really: {pointer to underlying array, length=3, capacity=5}A slice value itself is small — a pointer to an underlying array, a length (how many elements are currently "in" the slice), and a capacity (how much room the underlying array actually has before it needs to grow). This matters because slicing an existing slice (s[1:3]) doesn't copy data — it creates a new slice header pointing at the same underlying array, which means modifying elements through one slice can be visible through another slice that shares the same backing array.
a := make([]int, 3, 5) // len=3, cap=5 — room to grow without reallocating
b := append(a, 99) // fits within capacity — b shares a's underlying array
b[0] = 1000 // THIS ALSO CHANGES a[0] — they share memory!
c := make([]int, 3, 3) // len=3, cap=3 — NO room left
d := append(c, 99) // doesn't fit — Go allocates a NEW underlying array
d[0] = 1000 // c is UNAFFECTED — d now has its own separate arrayappend grows a slice's length, but whether it reuses the existing underlying array or allocates a brand-new one depends entirely on whether there's spare capacity — and that behavior is invisible just from reading the call site. Two slices that happen to share a backing array because one was created via slicing or a low-capacity append, can silently step on each other's data when one is modified — a genuinely subtle bug that's caught the best of Go developers by surprise, and doesn't show up in simple test cases that never happen to exceed a slice's capacity.
The practical defense: never assume two slice variables are independent just because they look like separately-declared variables — if one was derived from slicing or appending to another, check capacity behavior explicitly, or simply copy (copy(dst, src)) when independence is actually required.
m := map[string]int{"a": 1, "b": 2}
value, exists := m["a"] // the SECOND return value tells you if the key was present
if count, ok := m["c"]; ok {
fmt.Println(count)
} else {
fmt.Println("key not found") // m["c"] alone would just silently return 0, the zero value
}Go's map is a built-in hash map (the same structure Data Structures & Algorithms' own hash-map lessons covered conceptually), with one feature directly tied back to lesson 2's zero-value philosophy: looking up a missing key doesn't error or return undefined — it silently returns the value type's zero value, which is indistinguishable from a key that genuinely maps to that same zero value unless you use the two-value form (value, exists := m[key]) to check explicitly.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What three things does a Go slice value actually consist of?
2. Why is the append gotcha a genuinely subtle bug?
3. What does looking up a missing key in a Go map return by default?
Go