sync.Once and Lazy Initialisation
Run a setup function exactly once, no matter how many goroutines ask for it at the same moment.
var once sync.Once; once.Do(func() { /* runs exactly once */ })Some setup should happen once and only when it is first needed: opening a database pool, compiling a pattern, reading a file into memory. That is lazy initialisation. Doing it on first use is easy with one goroutine. With several, two of them can reach the "not ready yet" check at the same moment and both do the work.
How Once works
sync.Once has one method: Do(f func()). It runs f on the first call and never again for that Once. Any other goroutine that calls Do while f is still running blocks until f returns. That second guarantee is the important one. When Do returns, the work is finished, so the caller may use what f set up. The zero value is ready to use, and a Once must not be copied.
The shorthand
Go 1.21 added sync.OnceFunc(f func()) func(), sync.OnceValue(f func() T) func() T and sync.OnceValues(f func() (T1, T2)) func() (T1, T2). Each returns a function you call normally, and it wraps the same guarantees. sync.OnceValue replaces a Once plus a package-level variable, so prefer it for a value that is computed once.
What trips people up
Lazy initialisation is not always worth it. If the thing is needed on every request, build it in main and pass it in. Code is easier to test when its dependencies arrive as arguments rather than appearing from a global.
Example
package main
import (
"fmt"
"sync"
)
type client struct{ addr string }
var (
once sync.Once
instance *client
)
// get builds the client on the first call and returns the same
// one afterwards. Callers that arrive during the build wait.
func get() *client {
once.Do(func() {
fmt.Println("building client")
instance = &client{addr: "db:5432"}
})
return instance
}
func main() {
var wg sync.WaitGroup
for range 5 {
wg.Go(func() { _ = get() })
}
wg.Wait()
fmt.Println("addr:", get().addr)
}
// Output:
// building client
// addr: db:5432When to use it
- Opening a database connection pool the first time a handler needs it, not at import time.
- Parsing a large embedded data file into a lookup map on first use.
- Memoising an expensive per-object computation that several goroutines may request at once.
More examples
sync.OnceValue, the shorthand for the same thing
<code>getLonghand()</code> and <code>getShort()</code> behave identically: the body runs on the first call, later calls return the same pointer, and concurrent callers wait for the first. The short form has no package-level variable for other code to overwrite by mistake. Recognise both, because the longhand is everywhere in code written before Go 1.21.
type client struct{ addr string }
// Longhand: a Once, a variable to hold the result, and a getter.
var (
once sync.Once
instance *client
)
func getLonghand() *client {
once.Do(func() { instance = &client{addr: "db:5432"} })
return instance
}
// Shorthand (Go 1.21): OnceValue wraps both into one function.
var getShort = sync.OnceValue(func() *client {
return &client{addr: "db:5432"}
})Setup that can fail: keep the error too
The function passed to <code>Once.Do</code> returns nothing, so with a plain Once you must stash any error in a variable yourself. <code>sync.OnceValues</code> (Go 1.21) returns two values and remembers both. Note what it does not do: the work is not retried after a failure, and every later call gets the same error. If a retry matters, such as a network dial, do not use Once at all; build the thing at startup and fail loudly, or retry explicitly.
var loadRules = sync.OnceValues(func() ([]string, error) {
data, err := os.ReadFile("rules.txt")
if err != nil {
// The error is remembered and returned to every caller.
return nil, fmt.Errorf("load rules: %w", err)
}
return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
})
func main() {
rules, err := loadRules()
if err != nil {
log.Fatal(err)
}
fmt.Println(len(rules), "rules loaded")
}A Once per value, not per package
A Once tracks one job. Putting it in a struct gives every instance its own, which is how you memoise a per-object computation that several goroutines may request at the same time. Because the struct now contains a Once it must not be copied, so methods and callers use <code>*Report</code>.
type Report struct {
source []int
once sync.Once
total int
}
// Total sums the source the first time it is asked, then reuses it.
// Each Report has its own Once, so one report's work does not
// satisfy another's.
func (r *Report) Total() int {
r.once.Do(func() {
for _, n := range r.source {
r.total += n
}
})
return r.total
}
Discussion