Two goroutines assign different slices to one shared slice variable; what can a racing reader see?
answer
- a slice value is not one word
- pointer, length and capacity stored separately
- the reader can mix two assignments
- a length that never matched that array
- an interface is two words too
basics
~20 sA slice value is three words — pointer, length and capacity — stored one at a time. An unsynchronized reader can observe one assignment's pointer next to the other's length and index past the end of the real backing array.
solid answer
~50 sA slice variable is not one word: it holds a data pointer, a length and a capacity, and a plain assignment writes them as separate stores with no atomicity across them. A concurrent reader can therefore observe a **mixture** of two assignments — the pointer to a 4-byte array with the length of a 1024-byte one — a value that no goroutine ever stored. Indexing it reads memory past the array, so you get garbage bytes or a segmentation fault rather than a stale-but-valid slice. The same applies to an interface value (a type word plus a data word, so a method call can dispatch on one type against another's data) and to a string (pointer plus length). The fix is to publish the whole value indivisibly: guard it with a `sync.Mutex`, or store a pointer to an immutable value through an `atomic.Pointer[T]` so exactly one word is published.
code
go · 7 linesvar shared []byte // one header: data pointer, len, cap
go func() { shared = make([]byte, 4) }()
go func() { shared = make([]byte, 1024) }()
// A third goroutine reading shared may hold a header that says
// len 1024 over a 4-byte array, and index far past the array.go deeper
Remember that a slice value carries a pointer, a length and a capacity, and that assigning it copies all three. That is enough to see why a shared slice variable is not a single safe store.
Explain the multi-store mechanism and be able to name the three multiword types — slice, string, interface — and what a mixed read produces for each. Expect to be asked why single-word values behave differently.
Recognise the signature in an incident: a panic with an index far larger than any length the code sets, or field values that no code path could produce. Then propose the publication pattern — immutable value behind one atomic pointer — not just another lock.
Decide how shared read-mostly state is published across services: one convention for immutable snapshots, clear ownership of who may mutate, and a policy that keeps hot read paths lock-free without inviting torn values.
## Go values that are more than one word Most of Go's built-in types are a single machine word, so a plain assignment happens to be a single store. Pointers, maps, channels and function values are all one word. Three important types are not: | type | layout | |---|---| | slice | data pointer, length, capacity — **3 words** | | string | data pointer, length — **2 words** | | interface | type descriptor word, data pointer — **2 words** | Assigning one of these copies every word. The compiler emits several stores, and Go promises nothing about their atomicity or their order as seen by another goroutine. That is the whole mechanism behind a **torn value**. ## What tearing produces Suppose one goroutine does `shared = make([]byte, 4)` and another does `shared = make([]byte, 1024)`, with a third goroutine reading `shared` — none of them synchronized. A reader can load the pointer after the first store landed and the length after the second did. It now holds a slice header claiming 1024 bytes of a 4-byte array. `shared[900]` is then a read of whatever happens to sit after that array in memory: another object's fields, freed memory, or an unmapped page that faults. This is not a stale value; it is a value that was never written by anyone. Interfaces tear in a nastier way. An interface value is a pair: a word describing the dynamic type (and its method table) and a pointer to the data. If one goroutine stores a `*Circle` and another a `*Square`, a racing reader can end up with `*Circle`'s method table and `*Square`'s data pointer. Calling a method then jumps into the right function with the wrong receiver — the program does not crash at the call, it computes nonsense, possibly writing through a field offset that means something else entirely. This is one reason "it's only an interface assignment, worst case we read the old one" is wrong. Strings tear the same way as slices: a pointer from one store and a length from another gives a string that reads past the end of its bytes, which can expose unrelated memory in a log line or a response body. ## What about single-word values? A plain `int`, pointer or `bool` fits in a machine word, and on the hardware Go supports an aligned word-sized store is not split in half — the Go memory model explicitly guarantees that a racy read of a location no larger than a word observes *some* value actually written there, ruling out out-of-thin-air results. That guarantee is easy to over-read. It says nothing about *which* value, nothing about ordering relative to other variables, and it does not make the access legal: a racy single-word variable is still a data race, and the compiler is still entitled to hoist it into a register or reorder it because it may assume the program has no races. ## Publishing a multiword value safely There are three shapes that work. **A mutex around both sides.** The simplest and most general: the writer takes the lock to assign, every reader takes it to read. It costs a lock on the read path, which matters only if reads are extremely hot. **Publish a pointer atomically.** Build the new value completely, then swap in a pointer to it with one atomic store. Readers load the pointer atomically and then read the value it points at, which nobody mutates afterwards: ```go var cfg atomic.Pointer[Config] cfg.Store(newConfig) // one word published indivisibly c := cfg.Load() // readers see a fully built Config ``` `atomic.Pointer[T]` (Go 1.19 and later) is the typed form and handles alignment for you. The invariant that makes this correct is that the pointed-to value is treated as **immutable** after publication — if a writer later mutates a field of `*c`, the race is back. **Ownership.** One goroutine holds the value and hands copies or updates to others over a channel. Nothing is shared, so nothing can tear. ## What does not work - Making all the struct's fields `int64` "so each write is atomic" — the fields are still written by separate stores, so a reader can see a half-updated struct. - Having the reader copy the value into a local variable before using it — the copy itself is the racing read that tears. - Assuming tearing is a 32-bit-only problem. A slice header is three words on every architecture; being 64-bit does not make three stores one. ## Why this question is asked It separates candidates who know that "a data race gives you a stale value" from those who know that a data race can give you a value that never existed. The practical consequence is the debugging story: a panic with an index far beyond any length the code ever sets, or a method call that produces impossible field values, is a strong hint that a multiword variable is being assigned without synchronization.
- Does the same tearing risk apply to an interface variable, and what does it look like?Yes. An interface value is two words: the dynamic type with its method table, and a pointer to the data. A racing reader can pair one assignment's type word with another's data pointer, so a method call dispatches to the right function for the wrong value. Instead of a crash at the call site you get impossible field values, which is much harder to trace back to a missing lock.
- How do you let many goroutines read a config struct while one goroutine replaces it, with no torn reads?Build the replacement completely, then publish a pointer to it with a single atomic store — `atomic.Pointer[Config]` — and have readers load the pointer and read through it. Exactly one word is published, so readers always see a fully constructed value. The rule that makes it correct is that nobody mutates the struct after publishing; a later field write reintroduces the race.
- Is a racy read of a plain int equally dangerous?Less spectacular, but still a bug. An aligned word-sized read observes some value that was actually written, so you will not see a half-integer, but you have no guarantee about which value or about ordering relative to other variables, and the compiler may optimize on the assumption that no race exists. It must still be an atomic, mutex-guarded, or owned by one goroutine.
saying these in an interview costs you the question
- A single assignment statement in Go is atomic
- The worst case is reading a stale but valid slice
- Tearing only happens on 32-bit machines
- Making every struct field int64 makes the struct safe
- Copying the value into a local first avoids the race
- Only maps need locking, a shared slice variable is fine