Slices
Slices: the type you'll use for almost everything list-shaped
A slice looks similar to an array but has no fixed length:
names := []string{"Ada", "Grace", "Haleh"}
fmt.Println(len(names)) // 3Notice the type: []string, no number inside the brackets — that's what makes it a slice, not an array. Under the hood, a slice is a small struct holding a pointer to an underlying array, a length, and a capacity — but you rarely need to think about that to use one.
Growing a slice
var names []string // nil slice, len 0
names = append(names, "Ada")
names = append(names, "Grace")
fmt.Println(names) // [Ada Grace]append is a built-in function, not a method — you call append(slice, value), not slice.append(value), and you must reassign the result. This trips up everyone coming from a language with mutable list methods: append(names, "X") on its own does nothing to names, because a slice may need to be reallocated to grow (if the underlying array is full, append allocates a bigger one and copies everything over), so the only reliable way to see the new slice is the one append hands back.
Slicing a slice
The operation that gives slices their name — carving out a sub-range with s[low:high], low inclusive, high exclusive:
nums := []int{10, 20, 30, 40, 50}
fmt.Println(nums[1:3]) // [20 30]
fmt.Println(nums[:2]) // [10 20]
fmt.Println(nums[2:]) // [30 40 50]This is a view into the same underlying array, not a copy — mutating an element through one slice can be visible through another slice that overlaps it. This matters mostly when you pass slices between functions and both sides keep working with them; if you need an independent copy, use the built-in copy:
original := []int{1, 2, 3}
duplicate := make([]int, len(original))
copy(duplicate, original)make([]int, 3) creates a slice of length 3, pre-filled with the zero value (0 for int) — the idiomatic way to allocate a slice you're about to fill in, when you don't have literal values up front the way []int{1, 2, 3} does.
Iterating
for i, n := range nums {
fmt.Println(i, n)
}Same range you saw with control flow — it works identically over arrays and slices.
Strings are close cousins of byte slices
A Go string is UTF-8 encoded and immutable, and len(s) gives you the number of bytes, not necessarily the number of characters (a multi-byte character like é counts as more than one). Ranging over a string with for i, r := range s gives you Unicode code points (runes), not bytes — the one place Go's range behaves a little differently depending on what you're ranging over. You won't need this distinction often, but it's worth knowing it's there before it surprises you on non-ASCII input.