AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 04 — Arrays and Slices — Fixed Boxes and the Windows Onto Them

04 — Arrays and Slices — Fixed Boxes and the Windows Onto Them

August 13, 20267 min read
Download as Markdown

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]:

slice header ptr→ len3 cap5 01 2 3 4 56 len = 3 (indexable) cap = 5 (grow room before realloc) underlying array of 7 elements
  • 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 reallocation

This 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 channel

The 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!