Go, End to End
Arrays, Slices, and Maps
These three are where most of Go's genuinely surprising behavior lives, and getting a real mental model of how slices work under the hood pays off constantly.
Arrays: fixed size, value semantics
An array's length is part of its type: [3]int and [5]int are different types, incompatible with each other. Arrays are value types, assigning one array to another copies every element. Because of this, you'll rarely see raw arrays in idiomatic Go code outside of a few specific cases (fixed-size buffers, cryptographic hash outputs); slices do almost everything arrays do, more flexibly.
Slices: the real workhorse
A slice is a small struct under the hood: a pointer to an underlying array, a length, and a capacity. Slicing an existing slice or array doesn't copy data, it creates a new slice header pointing into the same backing array. This is the source of most slice surprises.
append and the reallocation gotcha
append grows a slice, but whether it reuses the existing backing array or allocates a new one depends on whether there's spare capacity. If capacity is exhausted, append allocates a new, larger backing array and copies everything over, meaning the returned slice is now backed by different memory than the one you passed in. This is exactly why you always reassign the result of append, s = append(s, x), never just call it and ignore the return value.
This is the single most common source of "why did modifying this slice change a completely different variable" bugs in Go. The rule to hold onto: two slices sharing a backing array is invisible from the outside, you can't tell just by looking at a variable's declaration whether it aliases another one's memory.
Maps
map[K]V is Go's hash map. Reading a missing key returns the value type's zero value, not an error, so you need the comma-ok idiom to distinguish "key absent" from "key present with a zero value". A nil map (the zero value of a map type) can be read from safely, but writing to it panics.
Map iteration order in Go is deliberately randomized between runs, on purpose, specifically so nobody accidentally depends on an order that was never guaranteed. If you need a stable order, sort the keys yourself.
Try it yourself: change the append example's make([]int, 3, 5) to make([]int, 3, 3) and predict whether a and d will still share memory before you run it.