What does sync.Once guarantee when ten goroutines call the same once.Do(setup) at once?
answer
- one runner, many waiters
- Do does not return early for the others
- the zero value needs no setup
- f runs at most once per Once value
basics
~10 sExactly one goroutine runs setup. The other nine block inside Do until that call has finished, then return. Every later call to Do on that same sync.Once returns immediately without running setup again.
solid answer
~50 s`sync.Once` has one job: the function you hand to `Do` runs at most once for that particular `Once` value. When ten goroutines race into `once.Do(setup)`, one of them wins and executes `setup`; the other nine do not skip ahead and they do not run their own copy — they block inside `Do` until the winner's `setup` has returned. That blocking is the whole point: it is why no caller can walk away with a half-built package variable. After that, `Do` is a cheap check that returns straight away, forever. The zero value is ready to use, so `var once sync.Once` needs no constructor, and each distinct `Once` value has its own independent "done" state. Two rules follow from the implementation: never copy a `sync.Once` after first use, and never call `Do` on the same `Once` from inside the function it is running — that deadlocks.
code
go · 11 linesvar (
once sync.Once
client *http.Client
)
func shared() *http.Client {
once.Do(func() {
client = &http.Client{Timeout: 5 * time.Second}
})
return client
}go deeper
Be ready to say both halves out loud: exactly one goroutine runs the function, and the others block until it has finished. Also know that var once sync.Once is immediately usable with no setup.
Explain why the blocking matters — without it a late caller could read a package variable the initialiser had not finished writing. Mention that Do returns nothing, so a failable initialiser must stash its error somewhere.
Show judgment about when a lazy global is the right call at all. Point out that Once cannot be reset, so any initialisation that can fail transiently needs a different primitive, and that the object's owner is often better placed in main.
Frame the tradeoff you are actually buying: a package-level Once makes a hidden global convenient and untestable in equal measure. Be able to say when you would push teams toward explicit construction and injection instead.
## What `sync.Once` is `sync.Once` is a tiny synchronisation object in Go's `sync` package with a single exported method: ```go func (o *Once) Do(f func()) ``` It exists to run a piece of one-time setup — opening a shared client, compiling a table, reading configuration — safely when many goroutines might trigger it at the same time. **The zero value is usable.** `var once sync.Once` is complete; there is no constructor and nothing to initialise. This matters because the typical use is a package-level variable that is never explicitly set up. ## The two guarantees 1. **At most one execution.** For a given `Once` value, `f` is called on the first `Do` and never again — not by the same goroutine, not by any other, not ever. 2. **Callers wait.** A goroutine that arrives while `f` is still running does not return early. It blocks inside `Do` until the running `f` has returned, and only then does `Do` return to it. The second guarantee is the one candidates forget, and it is the reason `sync.Once` exists at all. If late callers returned immediately, they would race ahead and read a package variable that the initialiser had not finished assigning. Because they block, by the time any `Do` call returns, the setup is complete. Note what is *not* promised: `Do` returns nothing. `f` is a `func()` with no parameters and no results, so an initialiser that can fail has to stash its error somewhere the accessor can read it. ## What it looks like in practice The canonical shape is an unexported package variable plus an accessor: ```go var ( once sync.Once client *http.Client ) func shared() *http.Client { once.Do(func() { client = &http.Client{Timeout: 5 * time.Second} }) return client } ``` Every caller goes through `shared()`. The first one builds the client; concurrent callers wait for it; everyone after that pays only the cost of `Do`'s fast path. ## Why not a plain boolean flag The hand-rolled version — `if !initialised { initialised = true; setup() }` — is broken twice over. Two goroutines can both observe `false` and both run `setup`, and the unsynchronised read and write of `initialised` is a data race that the race detector will flag on a `-race` build. `sync.Once` does the check-lock-recheck dance for you, and does it correctly. ## The rules that follow from the implementation `sync.Once` holds a done flag and a `sync.Mutex`. Three consequences: - **Do not copy it after use.** Copying the struct copies the done flag and the mutex, so the copy may believe setup already happened, or may run it a second time. Because it embeds a mutex, `go vet`'s `copylocks` check flags a copy of a `sync.Once` (or of any struct containing one), which is why such structs are passed as pointers. - **Do not call `Do` from inside `f` on the same `Once`.** The outer call holds the lock until `f` returns and the inner call waits for that lock, so the goroutine deadlocks. This is documented behaviour, not a bug to work around. - **There is no reset.** A `Once` has no `Reset` method and no way to arm it again. If your initialisation can fail and you want to retry it, `sync.Once` is the wrong primitive — you need a mutex-guarded state you control, or eager initialisation at startup. ## Scope is per value, not per function The "once" is a property of the `Once` variable, not of the function you pass. Two different `Once` values will each run their own `f`, even if it is literally the same function. Conversely, one `Once` guards one thing; if a package lazily builds three independent objects, that is three `Once` values (or three of the Go 1.21 helper functions), not one. ## When to reach for it Use `sync.Once` when the work is genuinely once-per-process, cheap to describe, and cannot fail in a way you would want to retry: memoising a compiled pattern, building a shared handle, registering something with a runtime. When the work can fail transiently, or when the object's lifetime should be owned by `main`, an explicit constructor passed down as a dependency is usually the better design — `sync.Once` makes a global convenient, but it is still a global.
- What happens if the function passed to once.Do calls Do on that same sync.Once?The goroutine deadlocks. The outer `Do` holds the `Once`'s internal mutex until `f` returns, and the nested `Do` waits for that same mutex, which will never be released. The `sync` package documents this explicitly. If two lazy initialisers genuinely depend on each other, give each its own `Once` and make sure the dependency runs to completion first.
- Is it safe to copy a struct that contains a sync.Once field?No. The copy carries a duplicate of the done flag and of the internal mutex, so it may either skip initialisation it never ran or run it a second time. `sync.Once` embeds a `sync.Mutex`, so `go vet`'s `copylocks` check reports the copy. Pass such structs by pointer, and never return one by value from a constructor after it has been used.
- Can you reset a sync.Once so its function runs again?Not through the API — there is no `Reset` method, and assigning a fresh `Once` over one that other goroutines may be calling is itself a data race. If you need repeatable initialisation, use a mutex guarding an explicit state (value, error, ready flag) that you can clear yourself, or rebuild the whole object and swap it in.
The first person into a dark room flips the switch while everyone else waits in the doorway until the light is actually on. After that nobody touches the switch again.
saying these in an interview costs you the question
- Says the other callers return from Do immediately
- Thinks sync.Once needs a constructor before first use
- Believes the once-ness is tied to the function, not the Once value
- Claims a sync.Once can be reset to run f again
- Passes a struct containing a sync.Once around by value