skip to content

Value Representation

What a slice, string, map or struct is in memory: a three-word header over a shared array, an immutable byte view, a control-word hash table, and fields padded to their alignment.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

21

Why does ranging over the same Go map twice give the entries in a different order?

level: juniorimportance: must knowfreq 68%

answer

  1. order was never part of the deal
  2. the runtime picks where to start
  3. a random starting point per loop
  4. deliberate, to break order dependence
  5. sort the keys when you need stability

basics

~20 s

Go's runtime starts every map iteration at a randomly chosen position, so map order is undefined and changes from loop to loop. For stable output, copy the keys into a slice, sort it, and index the map in that order.

solid answer

~40 s

A Go map is a hash table, and the language specification says the iteration order over a map is unspecified. The runtime goes further: it deliberately picks a random starting point in the table for each `for range` over a map, so two loops over the same unmodified map can hand back different orders, and so can two runs of the same binary. That is a feature rather than an accident — it stops code from quietly depending on an order the implementation never promised and that would change anyway as the map grows. The only guarantee is that every entry present for the whole loop is produced exactly once; entries added while you iterate may or may not show up. When you need determinism, sort the keys: `for _, k := range slices.Sorted(maps.Keys(m))`.

code

go · 11 lines
go
counts := map[string]int{"a": 1, "b": 2, "c": 3}

// Unordered: the sequence differs between loops and between runs.
for k, v := range counts {
	fmt.Println(k, v)
}

// Deterministic: a, b, c on every run.
for _, k := range slices.Sorted(maps.Keys(counts)) {
	fmt.Println(k, counts[k])
}

go deeper

for a junior

Be ready to say plainly that map iteration order in Go is undefined and randomised by the runtime, and to show the sort-the-keys workaround without hesitating.

for a middle

Explain that the runtime chooses a random starting point in the table for each loop, and state exactly what is guaranteed when entries are added or deleted while you iterate.

for a senior

Show where undefined order bites in production — non-reproducible output files, flaky golden tests, unstable log lines — and make the output deterministic at the boundary rather than hoping the order holds.

for a principal

Treat it as an API decision: any exported function returning results derived from a map either sorts them or documents the order as unspecified, so callers never build on an accident you will later change.

## The rule A Go map is a hash table: keys are hashed, and the hash decides where the entry is stored. Nothing about that storage relates to the order you inserted keys or to their sort order. The Go language specification states plainly that the iteration order over a map is not specified and is not guaranteed to be the same from one iteration to the next. The runtime does not merely leave the order unspecified — it randomises it on purpose. Every time a `for range` loop begins over a map, the runtime chooses a random place in the table to start, and walks from there, wrapping around. Two consecutive loops over a map that nobody touched in between can therefore produce two different sequences. ## Why randomise instead of just leaving it undefined? History. In early Go the order was merely undefined, and for a small map it happened to be stable in practice. People wrote code that relied on it — a printed report, a config dump, a hand-rolled cache eviction — and that code broke later, in production, once the map grew past a threshold and its entries were rehashed into different positions. Randomising converts a rare, delayed, production-only failure into an immediate and obvious one. If your test asserts a particular order, it fails on the first run rather than in six months. This is the same instinct behind the race detector: make the latent bug loud. ## What is actually guaranteed - Every entry that is present in the map for the entire loop is produced exactly once. - An entry deleted before the loop reaches it is not produced. - An entry added during the loop may or may not be produced — that is genuinely unspecified, and you should not write code either way. - The `len` of the map is not fixed for you at loop start; you are ranging over live storage. Mutating a map while ranging over it is legal in the single-goroutine sense (deleting entries during iteration is a common and supported pattern), but adding entries during iteration gives you an unpredictable set. ## Getting a deterministic order There is no ordered map in the standard library. Two patterns cover almost everything: **Sort the keys.** Collect the keys, sort them, then index the map. In recent Go this is one line, because `maps.Keys` yields an iterator over the keys and `slices.Sorted` collects and sorts it: ```go for _, k := range slices.Sorted(maps.Keys(counts)) { fmt.Println(k, counts[k]) } ``` For a key type that is not ordered — a struct, say — collect into a slice and use `slices.SortFunc` with your own comparison, or sort by a derived string. **Keep the order beside the map.** If what you want is insertion order rather than sort order, maintain a slice of keys and append to it when a key is inserted for the first time. The map gives constant-time lookup; the slice gives order. This is what an "ordered map" type would do for you internally. ## Where the undefined order actually bites The symptom is almost never "my loop printed things oddly". It is: - A golden-file test that passes locally and fails in CI, or fails one run in five. - Two runs of a batch job producing byte-different output files for identical input, defeating a downstream diff or a content hash. - A JSON payload whose object key order shifts every response, which is harmless for parsers but noisy in a recorded fixture. - A log line listing "active tenants" that a human is trying to compare across two deployments. The fix in each case is the same and belongs at the boundary: sort at the point where the data leaves the program, not deep inside the aggregation. ## A note on what randomisation is not It is not a security feature in the sense of a per-key secret hash for every workload, and it is not the garbage collector moving things around — Go's collector does not compact. It is simply a randomly chosen starting offset for the walk, cheap to compute and recomputed for each loop. It also costs you nothing to work with, as long as you internalise the one rule: **if the order matters, you produce it yourself.**

  • Can a single entry be produced twice in one range over a map?
    No. Every entry present for the whole loop is produced exactly once. The randomisation only chooses where the walk starts; it does not revisit slots. Entries added during the loop may or may not be produced, and entries deleted before the loop reaches them are not produced at all.
  • Why randomise the order rather than simply leaving it unspecified?
    Because merely unspecified order was stable enough in practice for small maps that people wrote code depending on it, then broke in production once the map grew and entries were rehashed. Randomising makes that dependency fail immediately, in development, instead of silently later.
  • What do you build when you need entries in insertion order?
    Keep a slice of keys alongside the map and append to it on first insert: the map gives constant-time lookup, the slice gives order. Go has no ordered-map type in the standard library, and sorting only helps when the order you want happens to be sort order.

Like dealing from a deck that is cut at a random spot each time: you see every card exactly once, but never in the same sequence.

saying these in an interview costs you the question

  • Says map iteration follows insertion order
  • Claims the order is stable within a single process run
  • Thinks you can sort a map itself rather than its keys
  • Assumes two maps with identical keys iterate identically
  • Blames the random order on the garbage collector
open as a page

Why must you write s = append(s, x) in Go instead of just calling append(s, x)?

level: juniorimportance: must knowfreq 85%

basics

~20 s

append returns a new slice value and never updates the slice you passed in. If the backing array has no spare room, append allocates a bigger array, so only the returned value is guaranteed to see the appended element.

open as a page

What three fields does a Go slice header hold at runtime, and what does each mean?

level: juniorimportance: must knowfreq 78%

basics

~20 s

A Go slice value is three words: a pointer to the first element it covers in a backing array, a length, and a capacity. The elements live in that array, not inside the slice value.

open as a page

What does a Go string value hold at runtime, and what does assigning one to another variable copy?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A Go string value is a two-word header: a pointer to an array of bytes plus a length. Assigning or passing a string copies only that header (16 bytes on a 64-bit platform), never the text it points at.

open as a page

Why can a Go function mutate a slice's elements but not change the caller's length?

level: middleimportance: must knowfreq 72%

basics

~20 s

A slice is passed by value, so the callee gets its own copy of the three-word header. Writes through the shared pointer reach the caller's elements; assigning a new length or capacity only changes the callee's copy.

open as a page

Why does converting between string and []byte in Go allocate and copy, and when does the compiler skip it?

level: middleimportance: must knowfreq 58%

basics

~20 s

Strings are immutable, so []byte(s) and string(b) each allocate a new array and copy the bytes, keeping the two independent. The compiler elides that copy only in read-only patterns such as m[string(b)] and string(b) == "literal".

open as a page

Why is a Go struct sometimes larger than the sum of its field sizes?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Go inserts padding bytes so each field starts at an address its type requires, such as an int64 on an 8-byte boundary. Padding follows declaration order, so the same fields in a different order can be smaller.

open as a page

How does the Go compiler decide a struct type's alignment and total size?

level: middleimportance: should knowfreq 35%

basics

~20 s

A struct's alignment is the largest alignment among its fields, and its size is rounded up to a multiple of that. The rounding adds tail padding so every element of an array of that struct stays aligned.

open as a page

Why is a Go map element unaddressable, making `&m[k]` and `m[k].n++` compile errors?

level: middleimportance: should knowfreq 46%

basics

~20 s

Inserting into a map can move existing entries to new storage, so a pointer into a map could be left dangling. Go therefore makes map elements unaddressable. Copy the value into a variable, change it, assign it back, or store pointers.

open as a page

What does Go's copy builtin return, and why does copying into make([]int, 0, 5) copy nothing?

level: middleimportance: should knowfreq 58%

basics

~20 s

copy returns how many elements it moved: the smaller of the two slices' lengths. A destination made with make([]int, 0, 5) has length zero, so there is nowhere to write and copy returns 0. Capacity is ignored.

open as a page

How does strings.Builder produce its final string without copying the bytes it accumulated?

level: middleimportance: should knowfreq 44%

basics

~20 s

strings.Builder appends into an internal byte slice and its String method builds a string header over that same array with unsafe.String, so there is no final copy. It is safe because a Builder only ever appends, never rewrites bytes it already handed out.

open as a page

How would you prove that struct padding, not live data, is inflating a Go service's heap?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Measure rather than guess. Compare unsafe.Sizeof of the struct against the sum of its field sizes, multiply the difference by the live instance count from a heap profile, and check that the product accounts for the missing memory.

open as a page

A daily aggregation job deletes every entry from its Go map, yet the heap never drops — why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Go maps never shrink. delete and clear release the stored keys and values but keep every slot the map ever allocated, so an emptied map keeps its peak footprint. Assign a freshly made map to reclaim it.

open as a page

A stage reads 4 MiB chunks from an io.Reader and keeps 32-byte subslices of each; live heap climbs to gigabytes. Why?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Each 32-byte subslice still points into its 4 MiB backing array, and the collector keeps a whole array alive while anything references any part of it. Copy what you retain with bytes.Clone, and the chunk becomes collectable.

open as a page

Why does returning a Go struct by value still share its []float64 field between callers?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Copying a struct copies a slice field's three-word header, not the elements. Both copies hold the same backing-array pointer, so a write through one is seen by the other. An array field like [16]float64 is copied element by element instead.

open as a page

A Go log pager truncates each line with s[:80] and users see mangled characters — what is happening and how do you cut safely?

level: seniorimportance: should knowfreq 38%

basics

~20 s

s[:80] cuts at byte 80, which can land inside a multi-byte UTF-8 sequence and leave a half-encoded rune that the terminal draws as a replacement character. Cut at a rune boundary instead, then worry about display width separately.

open as a page

Does the Go compiler reorder struct fields to eliminate padding?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

No. The gc toolchain lays fields out in declaration order, so removing padding is the programmer's job. Rust's default representation may reorder fields, which is one reason engineers arriving from it expect Go to compact a struct for them.

open as a page

How does Go's Swiss-table map layout differ from the old bucket-and-overflow-chain design?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Since Go 1.24 a map stores entries in groups of eight slots, each slot summarised by a control byte carrying part of its key's hash. One word-sized comparison tests all eight at once, replacing walks down chains of overflow buckets.

open as a page

When append outgrows a Go slice's capacity, how does the runtime pick the new capacity?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

The runtime allocates a larger array and copies into it. Small slices roughly double; past about 256 elements growth tapers toward 25 percent. The result is rounded up to an allocator size class, so the exact capacity is never guaranteed.

open as a page

Why should you use unsafe.SliceData instead of casting a slice to *reflect.SliceHeader?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

reflect.SliceHeader stores its Data field as a uintptr, which the garbage collector does not trace, so a hand-built header can name an already-collected array. unsafe.SliceData returns a typed pointer that keeps the backing array alive.

open as a page

What conditions would make you allow unsafe.String zero-copy conversions in your team's Go codebase?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Allow it only where a profile shows the conversion copy actually costs, where the bytes provably can never be written again, and where the trick stays inside one package behind an ordinary safe API. Everywhere else the standard says no.

open as a page