Collections
So far you've held one value at a time. Real programs deal in many - a list of users, a table of prices, the words in a sentence. One decision trips up newcomers: arrays versus slices. Clear that up first, and everything else falls into place.
Arrays vs slices - the distinction that matters
An array in Go is a fixed-size sequence of values, all the same type. The size is part of the type:
[3]int is "exactly three integers" - not two, not four. You can't grow it, which makes arrays rare in
everyday Go.
A slice is a flexible-length view onto a sequence of values. It can grow and shrink, and it's what
Go programmers use almost all the time - "a list that can change size." Its type has no number: []int
is "a list of integers, however many."
📝 Terminology. The empty brackets are the tell. [3]int (number inside) = array, fixed.
[]int (nothing inside) = slice, flexible. When in doubt, you want the slice.
Here's a slice in action:
package main
import "fmt"
func main()
$ go run main.go
[2 3 5 7]
2 7
[]int{2, 3, 5, 7} created a slice of four integers, printed in brackets. primes[0] reads the first
element (Go counts from zero), primes[3] the fourth - so we printed 2 then 7. Indexing past the end
(say primes[4]) crashes with an out-of-range error, since there's no fifth element.
Growing a slice with append
A slice's whole point is that it can grow. You do that with the built-in append function:
package main
import "fmt"
func main()
$ go run main.go
[Ada Alan Grace]
append(names, "Grace") produced a slice with "Grace" added on the end. The surprise: you assign the
result back to names. append doesn't always change the original in place - it may build a bigger
slice and hand it back - so the idiom is always names = append(names, ...). Skip the names = and
your addition vanishes.
You can append several at once, or even append one slice onto another:
names = append(names, "Linus", "Margaret")
append takes the slice first, then any number of new values, returning the grown slice. Same rule:
capture the result.
len and cap - length vs capacity
Two built-in functions tell you about a slice's size:
package main
import "fmt"
func main()
$ go run main.go
3 3
len(s) is the length - how many elements the slice holds right now (3). cap(s) is the
capacity - how many it could hold before Go must allocate a bigger block of memory. len is the one
you'll use constantly; cap is under-the-hood detail you'll mostly ignore until optimizing. They start
equal here, but after appends they can differ as Go grows backing storage in chunks.
Maps - looking things up by key
A slice is great for an ordered list, accessed by position. A map is for accessing things by name:
a lookup table storing key → value pairs. map[string]int reads as "a map from string keys to
integer values" - names to ages, say. (Other languages call this a dictionary, hash, or associative
array; same idea.)
package main
import "fmt"
func main()
$ go run main.go
36
map[Ada:36 Alan:41 Grace:28]
We created a map with two entries, looked up "Ada" to get 36, then added an entry by assigning to a
fresh key (ages["Grace"] = 28). Go printed them in tidy order here (because fmt sorts map keys when printing), but maps have no
guaranteed order - ranging over a map visits keys in randomized order, so don't rely on it.
When you look up a key that might not exist, use the two-value form to ask "did it exist?":
age, ok := ages["Nobody"]
fmt.Println(age, ok)
0 false
Reading a missing key doesn't crash - it returns the value type's zero value (0 for an int, from
phase 2) plus a boolean, ok, false when the key was absent. The
value, ok := m[key] pattern distinguishes "the value is genuinely 0" from "the key wasn't there at all."
⚠️ Gotcha - writing to a nil map panics. A map variable declared but never made is nil, and
writing to a nil map crashes your program at runtime:
var m map[string]int // declared, but nil - never made
m["x"] = 1 // panic!
panic: assignment to entry in nil map
var m map[string]int gives you a nil map - the zero value for maps. You can read from it (zero
values come back), but writing panics, since no table is allocated to store into. Fix: create it first
with make: m := make(map[string]int) (or a map literal like above). One of the most common first-week
Go panics - now you'll recognize it instantly.
📝 Terminology. A panic is Go's term for a runtime crash - the program stops with an error message and a trace, the runtime equivalent of an exception. Handling failure gracefully (the "errors are values" approach) comes in phase 7.
Looping over collections with range
To visit every element of a slice or every pair in a map, Go gives you range:
package main
import "fmt"
func main()
$ go run main.go
0 Ada
1 Alan
2 Grace
for i, name := range names walks the slice, handing you i, the index (starting at 0), and name,
the value at that position - the standard way to loop a slice. (Don't worry about the for keyword
yet - Go's single loop is phase 4; here it's just "do this for each
element.")
Often you only want the value, not the index. Use the blank identifier _ to throw the index away:
for _, name := range names
_ is Go's "I deliberately don't want this" placeholder. Since Go errors on unused variables
(phase 2), _ says "discard the index on purpose." Ranging a map works
the same way, giving key, value instead of index, value.
The slice-aliasing surprise
Here's the slice gotcha that bites everyone exactly once. A slice is a view onto an underlying block of data. When you slice a slice, both names can point at the same underlying data:
package main
import "fmt"
func main()
$ go run main.go
[99 2 3 4]
original[0:2] made part a window onto original's first two elements - not a copy. Changing
part[0] also changed original[0]; they share the same backing storage. Efficient (no copying) but
surprising the first time a slice changes "by itself."
⚠️ Gotcha. When you need an independent copy rather than a shared view, make one explicitly with the
built-in copy:
clone := make([]int, len(original))
copy(clone, original)
make([]int, len(original)) created a new slice of the same length, and copy filled it with
original's values. Now clone has its own storage - changing it leaves original untouched. Reach for
this whenever "I changed one and the other changed too" would be a bug.
Recap
[]Tis a slice (flexible, what you'll use);[N]Tis an array (fixed size, rare).appendgrows a slice - always assign the result back:s = append(s, x).lenis how many elements (you'll use it constantly);capis the under-the-hood capacity.- A map (
map[K]V) stores key→value pairs for instant lookup; usevalue, ok := m[key]to check if a key exists. - Writing to a nil map panics - create it first with
makeor a literal. rangeloops collections, givingindex, valuefor slices andkey, valuefor maps; use_to discard a part you don't need.- Slices can share underlying data (aliasing) - use
copywhen you need an independent copy.
Next: making decisions and organizing logic - Go's one loop, if and switch, and the
multiple-return-value functions that give Go its distinctive shape.
← Phase 2: Syntax, Values & Types · Guide overview · Phase 4: Control Flow & Functions →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. Which do you reach for almost always in Go - an array or a slice?
2. Writing to a nil map...