once
Before You Start
- Applies to: Go 1.21+ (examples verified with Go 1.22) · Prerequisites: sync.Mutex · Read time: about 8 min
sync.Once is a synchronization tool in Go's standard library, used to ensure that a function is executed only once in a concurrent environment. It's commonly used in scenarios such as lazy initialization, global resource initialization, etc., ensuring that a specific operation is executed only once, even when multiple goroutines are executing concurrently.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var once sync.Once
for i := range 10 {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() {
fmt.Println(i)
})
}()
}
wg.Wait()
}In the above code, there are a total of 10 goroutines, but regardless, only one goroutine will execute fmt.Println(i) in once.Do(). Which goroutine prints depends on which goroutine reaches once.Do() first.
Structure
type Once struct {
done atomic.Uint32
m Mutex
}Its internal structure is very simple:
done, an atomic value, used to indicate whether it has been executedm, mutex lock, used to block other goroutines that want to execute
Its principle is to lock before execution, then update done, and unlock after execution is complete.
Do
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}The code is overall very simple, with the following flow:
- Directly load the atomic value, if already executed, return immediately
- If not executed, try to acquire the lock, here multiple goroutines may be competing for the mutex
- After the winner successfully acquires the lock, it still needs to check the
donevalue again, because after you acquire the lock, someone else may have already completed the lock-acquire-execute-unlock sequence, and you were just awakened. - Execute the target function
- Update the
donevalue - Release the lock
Summary
sync.Once is a very concise and efficient synchronization tool. It ensures that a certain operation is executed only once in a concurrent environment, thereby avoiding duplicate work or resource waste.
Complete Example
The program below runs as-is: copy it into a file and execute go run main.go, or paste it into the Go Playground.
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
var wg sync.WaitGroup
// Five goroutines race to initialise, but only one runs the function
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() { fmt.Println("init once") })
}()
}
wg.Wait()
// Later calls are no-ops
once.Do(func() { fmt.Println("this will not print") })
fmt.Println("done")
}Expected output:
init once
doneCommon Pitfalls
Onceguards one function, not one call site. Use a separateOncevalue per initialisation.- A panic inside
Dostill marks the Once as done. Subsequent calls will not retry; make the initialiser robust or reset the design. - Do not copy a used
Once. It contains a mutex; copy semantics break the guarantee. - Do not wrap cheap work in
Do. The fast path uses atomics, but the surrounding pattern still costs a closure and a load. - Go 1.21+ has helpers.
sync.OnceFunc/sync.OnceValuewrap this pattern for you.
Change Log
- 2026-10-14: Added "Before You Start" notes, a complete runnable example with expected output, common pitfalls, and this change log.
