The fuse that never resets
You're building a service that needs to load a heavy configuration file. The file takes a second to parse. You have ten HTTP handlers that might need this config. You don't want to load it ten times. You also don't want to load it at startup if the user never requests it. You want the first request to trigger the load, and every subsequent request to use the result instantly.
Multiple goroutines might hit that handler at the exact same moment. If you just check a variable and load, two goroutines could both see the variable is empty and both start loading. You end up with duplicate work, wasted memory, or worse, a race condition where one goroutine overwrites the other's result.
sync.Once solves this. It guarantees a function runs exactly once, even when called concurrently by many goroutines. The first caller executes the function. Every other caller waits until it finishes, then returns immediately. Future calls skip the function entirely.
sync.Once is like a fuse. Once it blows, the circuit is permanently open. You can try to flip the switch a thousand times, but the current never flows again. In Go, the "current" is your initialization logic. The first call pays the cost. The rest get a free ride.
How it works under the hood
sync.Once wraps a function call. The Do method accepts a function with no arguments and no return values. The first time Do is called, it runs that function. Subsequent calls return without running the function.
The implementation uses a double-check locking pattern. Inside sync.Once, there is an atomic flag and a mutex. When Do is called, it reads the flag. If the flag is zero, it acquires the mutex. While holding the mutex, it checks the flag again. If the flag is still zero, it runs the function and sets the flag to one. If the flag is already one, it skips the function. The mutex is released.
This design makes the common case extremely fast. After initialization, Do performs a single atomic read of the flag and returns. There is no locking, no allocation, and no function call overhead. The first call pays the synchronization cost. Every call after that is just a memory read.
The first call pays the price. The rest get a free ride.
Minimal example
Here's the race: five goroutines fight to initialize a variable, and sync.Once picks the winner.
package main
import (
"fmt"
"sync"
)
var once sync.Once
var result string
// setupResult ensures the initialization logic runs exactly once.
func setupResult() {
once.Do(func() {
// The first goroutine to reach here executes this block.
// Other goroutines calling setupResult block until this returns.
result = "initialized"
fmt.Println("Loaded!")
})
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Multiple goroutines call this concurrently.
// Only one will trigger the initialization.
setupResult()
}()
}
wg.Wait()
fmt.Println(result)
}
The once variable is a package-level struct. The community convention is to name it once or prefix it with the resource name like dbOnce. Place it at package level. Export the getter function, not the variable. This keeps the initialization logic private while allowing other packages to access the result. Trust gofmt to align the struct fields. You don't need to format sync.Once manually.
Realistic example
Here's a lazy-loaded database connection in an HTTP handler.
package main
import (
"fmt"
"net/http"
"sync"
)
var dbOnce sync.Once
var dbConn string
// getDBConn initializes the connection on the first call and caches it.
func getDBConn() string {
dbOnce.Do(func() {
// Expensive setup runs exactly once.
// Concurrent requests wait here until setup completes.
dbConn = "postgres://localhost:5432/mydb"
fmt.Println("Database connected")
})
return dbConn
}
func handler(w http.ResponseWriter, r *http.Request) {
// Safe to call from multiple goroutines handling requests.
conn := getDBConn()
fmt.Fprintf(w, "Using: %s", conn)
}
func main() {
http.HandleFunc("/data", handler)
fmt.Println("Server ready")
}
Lazy loading saves resources. sync.Once makes it safe.
Handling errors
sync.Once doesn't support return values. The function signature passed to Do is fixed: func(). If your initialization can fail, you can't return the error. You have to capture the error in a variable.
This is a deliberate design choice. If Do returned values, the API would be ambiguous. The first call might return an error. Subsequent calls would return nothing. The caller would have to check if it's the first call to interpret the return value. Go prefers explicit error variables in this case.
Here's how to handle errors when Once swallows return values.
package main
import (
"fmt"
"sync"
)
var errOnce sync.Once
var initErr error
var config string
// loadConfig initializes the config and captures any error.
func loadConfig() {
errOnce.Do(func() {
// Simulate a failure.
// The error is stored in the package-level variable.
config, initErr = "", fmt.Errorf("disk full")
})
}
func getConfig() (string, error) {
loadConfig()
// Check the error after Do returns.
// The standard if err != nil pattern applies here.
if initErr != nil {
return "", initErr
}
return config, nil
}
func main() {
val, err := getConfig()
fmt.Println(val, err)
}
The error variable pattern requires the standard if initErr != nil check after calling the wrapper. This keeps the call site clean while preserving the error information. The community accepts the boilerplate because it makes the unhappy path visible.
Pitfalls and traps
The biggest trap is a panic. If the function passed to Do panics, sync.Once treats the panic as a successful completion. The internal flag is set. Every subsequent call to Do returns immediately without running the function. Your initialization failed, but sync.Once thinks it succeeded. The program likely crashes, but if you recover the panic, the initialization is permanently skipped.
Panic inside Do is a one-way ticket. Handle errors explicitly.
Another trap is copying. sync.Once must be used by value in the sense that the variable itself is the state. If you pass a sync.Once to a function by value, you copy the struct. The copy has its own internal state. Two goroutines might each hold a copy and both run the function. Always use a pointer to the struct or keep the sync.Once as a package-level variable. The compiler won't stop you from copying it. You get cannot take the address of... only if you try weird pointer tricks, but copying is legal and dangerous. The race detector catches the concurrent access to the copied state. If you copy a sync.Once and use it concurrently, the race detector reports a data race on the internal fields. The compiler accepts the code without error.
sync.Once cannot be reset. There is no Reset method. If you need to re-initialize, you must replace the Once instance. A common pattern is to wrap Once in a struct and replace the struct, or use a pointer to Once and swap the pointer. This is rare. Usually, if you need reset, you don't need Once. You need a mutex and a flag.
Copy Once and you break the contract.
When to use sync.Once
Use sync.Once when you need lazy initialization that is safe for concurrent access. Use init() when the initialization must happen before main starts and doesn't depend on runtime arguments. Use a sync.Mutex with a flag when you need to re-initialize the value later, since sync.Once cannot be reset. Use a simple variable assignment when only one goroutine ever accesses the value during startup.
Pick the tool that matches the lifecycle. Once is for once.