04 — Arrays and Slices — Fixed Boxes and the Windows Onto Them
Two types that look similar and behave differently kept tripping me until I saw the mechanism. The model that finally separated them: an array is a fixed-size box whose length is part of its type, while a slice is a small header — a pointer plus a length and a capacity — that acts as a window onto an underlying array. [1][2] Almost all Go code uses slices; arrays exist mostly as the backing storage slices sit on top of. Once I could see the header pointing at the box, the confusing behaviors — aliasing, capacity growth, the difference between make([]T, 5) and make([]T, 5, 10) — all snapped into place.
Arrays — the fixed box
An array has a fixed length, and that length is part of the type [1]:
var a [3]int // [3]int is a distinct type from [5]int
b := [3]int{1, 2, 3}Two consequences follow from "length is part of the type." First, you cannot resize an array — [3]int is forever a three-element type. Second, arrays are value types: assigning one array to another, or passing one to a function, copies the entire contents [1]. That is rarely what you want, which is why raw arrays show up almost exclusively as the backing storage for slices and in memory-layout-critical code.
Slices — the header pointing at a box
A slice is the dynamic, everyday sequence type. The key insight is that a slice is _not_ an array — it is a small header with three fields [2]:
- pointer — the address of the first element the slice can see
- len — how many elements are currently indexable
- cap — how many elements the underlying array holds from that pointer onward
Because the slice is just a header, copying a slice (assigning it, passing it to a function) copies the three-word header — cheap — and _both copies point at the same underlying array [2]. Two slices can alias the same storage, which is the source of both the power (sub-slicing is free) and the danger (modifications through one alias are visible through the other).
Length, capacity, and growth
The len/cap distinction matters most when you append. A slice can grow up to its capacity without any reallocation — the new element just claims unused room in the underlying array. Once append would exceed cap, Go allocates a _new, larger_ underlying array, copies the old elements over, and re-points the slice header at it [3].
The growth strategy roughly doubles capacity for smaller slices and grows more conservatively for large ones. If you know the final size in advance, pre-allocate to avoid the repeated copy:
s := make([]int, 0, 1000) // len 0, cap 1000 — 1000 appends with no reallocationThis single habit — passing a capacity hint to make — is one of the easiest performance wins in Go.
make — initializing reference types
make is the built-in for creating and initializing slices, maps, and channels [4]. Unlike new (which returns a zeroed pointer), make returns a usable value already wired up. For slices it takes a length and an optional capacity:
a := make([]int, 5) // len 5, cap 5 — five zero-value ints
b := make([]int, 5, 10) // len 5, cap 10 — five indexable, room for five more
c := make(map[string]int) // empty, ready-to-use map
d := make(chan int) // unbuffered channelThe confusion to avoid: make([]int, 5) gives you a slice of length 5, so all five elements already exist as zeros and append would add a sixth. If you wanted an empty slice with grow room, use make([]int, 0, 5).
Converting between arrays and slices
Two newer conversions close the loop. An array becomes a slice for free via the slice expression arr[:] or arr[start:end] — this creates a header pointing at the array's memory with no copying, so mutations through the slice affect the array [5]. Going the other way, Go 1.17+ lets you convert a slice to a fixed-size array with [N]T(slice) — this _does_ copy the data, and panics if the slice has fewer than N elements [5].
String literals and conversions (a related aside)
Two literal forms appear alongside array/slice work. Raw string literals use backticks and interpret nothing — no \n, no escapes — which makes them ideal for regex, SQL, JSON templates, and multi-line text where escaping would be noisy [6]. Interpreted string literals use double quotes and process escape sequences like \n and \t — the everyday choice for text needing control characters [7]. And the ubiquitous conversion in this neighborhood is between strings and byte slices: []byte(s) gives you the UTF-8 bytes to manipulate, and string(bs) turns them back. That pair is how you do byte-level work on a string.
How I use this
Three habits fall out of the header model. I almost never write [N]T directly — arrays belong to memory layouts and the rare fixed-size buffer; for any sequence I reach for a slice. When I know the eventual size, I pass a capacity to make to skip the realloc churn. And before mutating a slice that came from somewhere else, I check whether I am sharing its underlying array — if a caller might still hold a view onto the same storage, a defensive copy into a fresh slice prevents surprising aliasing bugs. The header-pointing-at-box picture is what makes all three decisions obvious.
References
[1] The Go Authors, "Go Tour: Arrays," go.dev, 2024. [Online]. Available: https://go.dev/tour/moretypes/6
[2] The Go Authors, "Go Tour: make," go.dev, 2024. [Online]. Available: https://go.dev/tour/moretypes/13
[3] A. Devb, "Understanding Go's Slice Data Structure and Its Growth Pattern," Medium, 2024. [Online]. Available: https://medium.com/@arjun.devb25/understanding-gos-slice-data-structure-and-its-growth-pattern-48fe6dd914b4
[4] freeCodeCamp, "The new() vs make() Functions in Go," 2024. [Online]. Available: https://www.freecodecamp.org/news/new-vs-make-functions-in-go/
[5] Labex, "How to slice arrays correctly in Go," 2024. [Online]. Available: https://labex.io/tutorials/go-how-to-slice-arrays-correctly-418936
[6] The Go Authors, "Strings in Go," The Go Blog, 2013. [Online]. Available: https://go.dev/blog/strings#what-is-a-string
[7] DigitalOcean, "An introduction to working with strings in Go," 2024. [Online]. Available: https://www.digitalocean.com/community/tutorials/an-introduction-to-working-with-strings-in-go
Knowledge check · Question 1 of 5
Why is the length part of an array's type but not a slice's type?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!