> series: learn go from 0 to 100 | article 8 of 19
Seven articles in and you’ve been using Go slices without knowing what they really are. Today we pop the hood, because the difference between using Go and understanding Go lives exactly here: knowing what’s underneath a []int.
Arrays: the foundations you rarely touch
Go’s array is fixed size and behaves as a VALUE (A Tour of Go, arrays):
// Array: fixed size, part of the type.
// [3]int and [4]int are DIFFERENT types.
var a [3]int
// And it's copied WHOLE on assignment or function call.
b := a
b[0] = 99
fmt.Println(a[0]) // 0: a never noticed
Full copy on assignment: nothing like Java or Python, where you pass references around. That’s why day to day almost nobody uses arrays directly. They’re the foundations Go slices live on.
Slices: three fields and an array behind
A slice is a tiny structure with three fields: a pointer to the underlying array, a length and a capacity (Go Slices: usage and internals):
s := make([]int, 3, 5) // type, length, capacity
fmt.Println(len(s), cap(s)) // 3 5
// append returns a NEW slice: always reassign.
s = append(s, 42)
// If cap runs out, Go allocates a bigger array
// and copies. Otherwise it reuses the same one.
len is what you see, cap is how much it can grow without moving. When append exhausts capacity, it allocates a bigger array and copies. That’s why append returns the slice: it may point somewhere new (A Tour of Go, slices).

The shared array gotcha
A direct consequence of that internal pointer, and an endless source of subtle bugs:
original := []int{1, 2, 3, 4, 5}
chunk := original[1:3] // [2 3]
chunk[0] = 99
fmt.Println(original) // [1 99 3 4 5] surprise!
// They share the underlying array. To detach:
clone := make([]int, len(chunk))
copy(clone, chunk)
Slicing with s[a:b] copies no data: it creates another header looking at the SAME array. Cheap and blazing fast, until two chunks step on each other. When you need real independence, copy and sleep well.
Maps: the built-in dictionary
Go’s map is the hash table you already know (A Tour of Go, maps and Effective Go):
ages := map[string]int{"ada": 36, "linus": 55}
// Reading a missing key does NOT blow up:
// you get the zero value. Comma ok tells them apart.
age, ok := ages["grace"]
if !ok {
fmt.Println("no grace here")
}
delete(ages, "linus")
// Iteration: order is NOT guaranteed.
for name, age := range ages {
fmt.Println(name, age)
}
Three things that will save you grief:
- Reading a missing key returns the zero value, no exception, and comma ok is your friend.
- Writing to a nil map (declared without make or a literal) panics.
- Iteration order is unspecified (spec) and the runtime deliberately varies it between runs. If your code depends on map order, your code has a bug you haven’t met yet.
And article 3‘s mystery: strings
A string is an immutable sequence of UTF-8 BYTES, not characters (Strings, bytes, runes and characters in Go):
s := "caña"
fmt.Println(len(s)) // 5, not 4: ñ takes 2 bytes
// range decodes runes, not bytes:
for i, r := range s {
fmt.Printf("%d:%c ", i, r) // 0:c 1:a 2:ñ 4:a
}
Indexing with s[i] gives you raw bytes, but range decodes rune by rune, which is why indexes jump (spec, range). Article 3‘s challenge mystery ¡solved!.
Weekly challenge: put Go slices to work
- Predict on paper the len and cap of a slice after five successive append calls starting from make([]int, 0, 2). Then verify by printing at each step.
- Reproduce the shared array gotcha and fix it with copy.
- Word counter: read a text and build a map[string]int with each word’s frequency using strings.Fields.
- Bonus: print “programación” byte by byte and rune by rune. Count how many bytes the ó takes.
Go slices: level complete.
Next article: idiomatic error handling. The famous if err != nil and why gophers like it that way.
> exit