What does Go's memory model guarantee about writes made before a `go` statement?
answer
- a one-way street
- the launch itself is the edge
- captured values are visible on entry
- nothing orders the way back out
- writes after the launch are not covered
basics
~20 sEverything the starting goroutine wrote before the go statement is guaranteed visible to the new goroutine when it begins running. The guarantee is one-way: the starter is not guaranteed to see anything the new goroutine writes.
solid answer
~50 sGo's memory model says the `go` statement is synchronized before the start of the new goroutine's execution. So every write the starting goroutine had already made — filling a struct, building a slice, assigning a package-level pointer, and the values the closure captured — is guaranteed visible inside the goroutine body. That is why handing data *into* a goroutine at launch needs no lock. There is no matching edge in the other direction: neither the goroutine's writes nor its exit are ordered with anything the starter does afterwards, so reading a result it produced requires a real edge — receiving from a channel it sent on, or a `sync.WaitGroup.Wait` that its `Done` unblocked. Writes the starter makes *after* the `go` statement are concurrent with the goroutine and are a data race unless something orders them.
code
go · 7 linesfunc start() {
cfg := loadConfig()
go func() {
serve(cfg) // guaranteed to observe loadConfig's result
}()
cfg = nil // racy: nothing orders this against the read above
}go deeper
Be ready to state the rule in one sentence: whatever you set up before starting a goroutine is visible inside it, and getting a value back out needs a channel or a WaitGroup.
Explain why the rule is one-way, and name the edge that carries data back. Expect to be asked what happens to a variable the parent keeps writing after the launch.
Show you use this deliberately: configuration built before the workers start needs no locking, and you can defend that in review instead of adding a mutex out of habit.
Own the convention this implies for shared state across a codebase — publish once before launching readers, or hand it over a channel, and treat any post-launch mutation of shared setup state as a design smell.
## What "happens-before" is doing in Go The Go memory model exists to answer one question: when is a read of a variable *guaranteed* to observe a particular write to it? It answers by defining an ordering. Inside a single goroutine, statements are ordered by the program text (Go calls this *sequenced before*). Across goroutines, only specific synchronization operations create ordering (*synchronized before*). Chain those two together transitively and you get **happens-before**. The payoff rule is simple. If a write to a variable happens-before a read of that variable, and no other write is sandwiched between them, the read is guaranteed to observe that write. If a write and a read of the same variable are **not** ordered by happens-before, they are concurrent, the program contains a **data race**, and Go promises you nothing useful: the compiler is free to keep the variable in a register and never reload it, and the hardware is free to make the store visible late or out of order relative to other stores. ## The `go` statement is one of those synchronization operations The model states it directly: *the `go` statement that starts a new goroutine is synchronized before the start of the goroutine's execution.* Everything sequenced before the `go` statement in the starting goroutine therefore happens-before the new goroutine's very first line. That is worth more than it first appears. It covers: - values the closure captured, and the memory those values point at; - arguments evaluated at the `go` statement (they are evaluated in the starting goroutine, before the goroutine begins); - package-level state the starter had already written; - everything the starter itself learned through *earlier* edges — happens-before is transitive, so if the starter received a value from a channel and then launched a goroutine, the new goroutine also observes whatever that send ordered. So the common shape below is correct without any lock: ```go cfg := loadConfig() go func() { serve(cfg) }() // serve is guaranteed to see loadConfig's result ``` ## The guarantee is one-way, and that is the part people miss There is no rule saying the goroutine's writes are ordered before anything the starter does, and there is explicitly no rule ordering a goroutine's **exit**. Two mistakes follow from forgetting that. **Mistake one: writing after the launch.** Anything the starter writes *after* the `go` statement is concurrent with the goroutine. In: ```go cfg := loadConfig() go func() { serve(cfg) }() cfg = nil // race: nothing orders this against serve's read ``` the assignment races with the closure's read of `cfg`. The goroutine may observe either value, and a racy program is not merely non-deterministic — the compiler was allowed to optimise on the assumption there was no race. **Mistake two: reading the result back.** A goroutine that computes something into a shared variable and returns has established nothing. The starter needs an explicit edge to observe it: receive from a channel the goroutine sent on (or closed), let a `sync.WaitGroup.Wait` be unblocked by its `Done`, or take a mutex the goroutine unlocked. A `time.Sleep` is not a synchronization operation, no matter how long it is. ## Related edges that complete the picture Package initialization is also ordered: all `init` functions and package-level variable initialization complete before `main.main` starts, so anything set up there is visible to every goroutine the program later starts. Combined with the `go`-statement rule, this gives you the cheapest correct pattern for read-mostly configuration: **initialize it before you start the readers**, and never write it again. No lock is needed, because the ordering is supplied by the launch itself. ## How to say this in an interview The compressed version is: *starting a goroutine is an ordering edge; finishing one is not.* Data flows in for free at launch; data flowing back out has to be carried by a channel, a `WaitGroup`, or a mutex. If you can state that, and then point out that writes made after the `go` statement fall outside the guarantee, you have the whole rule.
- Once that goroutine returns, is the starter guaranteed to see what it wrote?No. Go's memory model says a goroutine's exit is not synchronized before any event in the program. You need an explicit edge: receive from a channel the goroutine sent on or closed, let a `sync.WaitGroup.Wait` be unblocked by its `Done`, or acquire a mutex it unlocked. The return statement itself orders nothing.
- Does the guarantee also cover writes the starter makes after the `go` statement?No. Those writes are concurrent with the new goroutine. If the goroutine reads the same variable, that is a data race and it may observe either value — or a value the compiler cached in a register. Only writes sequenced before the `go` statement are ordered.
- Where does package-level initialization fit into this?Package-level variable initialization and all `init` functions complete before `main.main` starts, so anything set up there is visible to every goroutine the program starts afterwards. That is why read-only configuration assigned during initialization needs no lock at all.
saying these in an interview costs you the question
- Claims the new goroutine needs a mutex to read setup data
- Says the starter automatically sees results once the goroutine returns
- Thinks a sleep after the go statement is enough to read a result back
- Believes writes made after the go statement are ordered too
- Treats the ordering as symmetric between the two goroutines