1. Arrays vs. Slices
An array has a fixed length baked into its type —
[3]int and [4]int are different, incompatible types. In
practice, almost nobody uses arrays directly; a slice — a
lightweight view over an underlying array, with a length and a capacity — is what
Go code actually uses.
var arr [3]int = [3]int{1, 2, 3} // fixed size, part of the type
nums := []int{1, 2, 3} // a slice literal — no size in the type
fmt.Println(len(nums), cap(nums)) // 3 3
more := make([]int, 3, 10) // length 3, capacity 10
fmt.Println(len(more), cap(more)) // 3 10
A slice is really three things bundled together: a pointer to a backing array, a length (how many elements are in use), and a capacity (how many elements the backing array can hold before it needs to grow). Understanding that triple is the key to everything else this week.
2. append, Capacity & Shared Backing Arrays
append adds an element and returns a slice — but whether that returned
slice shares memory with the original depends entirely on whether capacity was
available.
original := make([]int, 3, 5) // len 3, cap 5 — room to grow
a := original
b := append(original, 99) // capacity available: b shares original's backing array
b[0] = 1
fmt.Println(a[0]) // 1 — a changed too! They share memory.
full := make([]int, 3, 3) // len 3, cap 3 — no room
c := append(full, 99) // capacity exceeded: Go allocates a brand-new backing array
c[0] = 1
fmt.Println(full[0]) // unchanged — c has its own memory now
When append has spare capacity, it writes into the existing backing
array and returns a slice pointing at the same memory — any other slice still
viewing that array sees the change. When it doesn't, Go allocates a new, larger
backing array (typically doubling capacity) and copies the data over, fully
decoupling the result from the original. This is the single most common source of
"why did an unrelated slice just change" bugs in Go.
3. Maps: comma-ok, Iteration Order & Zero Values
ages := map[string]int{"Asha": 30, "Ravi": 25}
ages["Priya"] = 28 // insert
delete(ages, "Ravi") // remove
fmt.Println(ages["Zed"]) // 0 — a missing key returns the zero value, no error
That last line is the trap: ages["Zed"] returns 0 whether
"Zed" is actually mapped to 0, or isn't in the map at all. The
comma-ok idiom is how you tell the two apart.
value, ok := ages["Zed"]
if !ok {
fmt.Println("Zed is not in the map")
} else {
fmt.Println("Zed's age is", value)
}
And a second trap: iterating a map with for k, v := range m visits keys
in an intentionally randomized order — different on every run, by
design, so code never accidentally comes to depend on an order maps don't actually
guarantee. Sort the keys explicitly first if order matters.
4. Choosing the Right Collection
- Array — rarely used directly; reach for one only when a fixed, compile-time-known size is itself meaningful (a 3x3 game board, a fixed-size hash).
- Slice — the default for any ordered, growable list.
- Map — key-based lookup where order doesn't matter, or you explicitly sort when it does.
When passing a slice to a function that might grow it, remember append
can return a different backing array than the one passed in — the idiomatic pattern
is always to reassign the result (s = append(s, x)), never to assume
the original variable was mutated in place.
5. Hands-on Exercise
Build a word-frequency counter
A small program exercising slices, append, and the comma-ok map idiom together.
Requirements:
- A function
WordCounts(words []string) map[string]intthat returns how many times each word appears. - Use the comma-ok idiom explicitly inside
WordCountsto increment an existing count vs. initialize a new one — don't rely on the zero-value shortcut without understanding why it also happens to work here. - A function
TopN(counts map[string]int, n int) []stringthat returns thenmost frequent words, built by appending to a slice and sorting it. - A short comment demonstrating (with a code snippet, not just prose) the shared-backing-array behavior from this week's second section, using a slice you construct in
main. - Confirm your program produces the same top-N words regardless of how many times you run it, despite iterating a map internally.
If your top-N results seem to differ between runs even though the input is identical, you're likely iterating the counts map directly to build the results slice — map iteration order is randomized by design, so collect into a slice first and sort that slice by count (and by word, as a tiebreaker) rather than trusting iteration order to be stable.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does [3]int and [4]int being different types matter in practice, and why do most Go programs avoid arrays as a result?
Why does [3]int and [4]int being different types matter in practice, and why do most Go programs avoid arrays as a result?
Because the length is part of the type itself, a function written to accept [3]int cannot accept a [4]int — there's no way to write a function generic over array length without generics (and even then it's awkward). A slice has no length in its type, so one function signature like func Sum(nums []int) int works for a slice of any length, which is why slices are the practical default.
Q2
Given b := append(a, x), what determines whether mutating b also changes a?
Given b := append(a, x), what determines whether mutating b also changes a?
Whether a had spare capacity beyond its length at the time of the append. If it did, append writes the new element into the same backing array and b shares memory with a — mutating one is visible through the other. If capacity was exhausted, Go allocates a new backing array for b, and the two become fully independent.
Q3
Why does ages["Zed"] returning 0 not tell you whether "Zed" is actually in the map?
Why does ages["Zed"] returning 0 not tell you whether "Zed" is actually in the map?
A map lookup for a missing key returns the zero value for the map's value type rather than an error or a special sentinel — for map[string]int, that's 0, which is indistinguishable from a key that's genuinely mapped to 0. The comma-ok form (value, ok := ages[key]) is the only reliable way to tell the two cases apart.
Q4
Why is it considered a bug to write code that depends on the order for k, v := range someMap visits keys in?
Why is it considered a bug to write code that depends on the order for k, v := range someMap visits keys in?
Go deliberately randomizes map iteration order on every run specifically so code can't accidentally come to rely on an ordering the language never promised — relying on it anyway means the program's behavior becomes nondeterministic and can change between runs, or between Go versions, with no warning. Anything that needs a stable order has to sort the keys explicitly.