sync.Once: Go's Simple Pattern for Safer Concurrency
Daniel Hayes
Full-Stack Engineer Β· Leapcell

π The Essence of Go Concurrency: A Comprehensive Guide to sync.Once Family
In Go concurrent programming, ensuring an operation is executed only once is a common requirement. As a lightweight synchronization primitive in the standard library, sync.Once solves this problem with an extremely simple design. This article takes you to a deep understanding of the usage and principles of this powerful tool.
π― What is sync.Once?
sync.Once is a synchronization primitive in the Go language's sync package. Its core function is to guarantee that a certain operation is executed only once during the program's lifecycle, regardless of how many goroutines call it simultaneously.
The official definition is concise and powerful:
Once is an object that ensures a certain operation is performed only once. Once the Once object is used for the first time, it must not be copied. The return of the f function "synchronizes before" the return of any call to once.Do(f).
The last point means: after f finishes executing, its results are visible to all goroutines that call once.Do(f), ensuring memory consistency.
π‘ Typical Usage Scenarios
- Singleton pattern: Ensure that database connection pools, configuration loading, etc., are initialized only once
- Lazy loading: Load resources only when needed, and only once
- Concurrent safe initialization: Safe initialization in a multi-goroutine environment
π Quick Start
sync.Once is extremely simple to use, with only one core Do method:
package main import ( "fmt" "sync" ) func main() { var once sync.Once onceBody := func() { fmt.Println("Only once") } // Start 10 goroutines to call concurrently done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done } }
The running result is always:
Only once
Even if called multiple times in a single goroutine, the result is the same β the function will only execute once.
π In-depth Source Code Analysis
The source code of sync.Once is extremely concise (only 78 lines, including comments), but it contains an exquisite design:
type Once struct { done atomic.Uint32 // Identifies whether the operation has been executed m Mutex // Mutex lock } func (o *Once) Do(f func()) { if o.done.Load() == 0 { o.doSlow(f) // Slow path, allowing fast path inlining } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done.Load() == 0 { defer o.done.Store(1) f() } }
Design Highlights:
-
Double-Check Locking:
- First check (without lock): quickly determine if it has been executed
- Second check (after locking): ensure concurrent safety
-
Performance Optimization:
- The done field is placed at the beginning of the struct to reduce pointer offset calculation
- Separation of fast and slow paths allows inlining optimization of the fast path
- Locking is only needed for the first execution, and subsequent calls have zero overhead
-
Why not implement with CAS?οΌ The comment clearly explains: A simple CAS cannot guarantee that the result is returned only after f has finished executing, which may cause other goroutines to get unfinished results.
β οΈ Precautions
-
Not copyable: Once contains a noCopy field, and copying after the first use will lead to undefined behavior
// Wrong example var once sync.Once once2 := once // Compilation will not report an error, but problems may occur during runtime
-
Avoid recursive calls: If once.Do(f) is called again in f, it will cause a deadlock
-
Panic handling: If a panic occurs in f, it will be regarded as executed, and subsequent calls will no longer execute f
β¨ New Features in Go 1.21
Go 1.21 added three practical functions to the sync.Once family, expanding its capabilities:
1. OnceFunc: Single-execution function with panic handling
func OnceFunc(f func()) func()
Features:
- Returns a function that executes f only once
- If f panics, the returned function will panic with the same value on each call
- Concurrent safe
Example:
package main import ( "fmt" "sync" ) func main() { // Create a function that executes only once initialize := sync.OnceFunc(func() { fmt.Println("Initialization completed") }) // Concurrent calls var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() initialize() }() } wg.Wait() }
Compared with the native once.Do: When f panics, OnceFunc will re-panic the same value on each call, while the native Do will only panic on the first time.
2. OnceValue: Single calculation and return value
func OnceValue[T any](f func() T) func() T
Suitable for scenarios where results need to be calculated and cached:
package main import ( "fmt" "sync" ) func main() { // Create a function that calculates only once calculate := sync.OnceValue(func() int { fmt.Println("Start complex calculation") sum := 0 for i := 0; i < 1000000; i++ { sum += i } return sum }) // Multiple calls, only the first calculation var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println("Result:", calculate()) }() } wg.Wait() }
3. OnceValues: Supports returning two values
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
Perfectly adapts to the Go function's idiom of returning (value, error):
package main import ( "fmt" "os" "sync" ) func main() { // Read file only once readFile := sync.OnceValues(func() ([]byte, error) { fmt.Println("Reading file") return os.ReadFile("config.json") }) // Concurrent reading var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() data, err := readFile() if err != nil { fmt.Println("Error:", err) return } fmt.Println("File length:", len(data)) }() } wg.Wait() }
π Feature Comparison
Function | Characteristics | Applicable Scenarios |
---|---|---|
Once.Do | Basic version, no return value | Simple initialization |
OnceFunc | With panic handling | Initialization that needs error handling |
OnceValue | Supports returning a single value | Calculating and caching results |
OnceValues | Supports returning two values | Operations with error returns |
It is recommended to use the new functions first, as they provide better error handling and a more intuitive interface.
π¬ Practical Application Cases
1. Singleton Pattern Implementation
type Database struct { // Database connection information } var ( dbInstance *Database dbOnce sync.Once ) func GetDB() *Database { dbOnce.Do(func() { // Initialize database connection dbInstance = &Database{ // Configuration information } }) return dbInstance }
2. Lazy Loading of Configuration
type Config struct { // Configuration items } var loadConfig = sync.OnceValue(func() *Config { // Load configuration from file or environment variables data, _ := os.ReadFile("config.yaml") var cfg Config _ = yaml.Unmarshal(data, &cfg) return &cfg }) // Usage func main() { cfg := loadConfig() // Use configuration... }
3. Resource Pool Initialization
var initPool = sync.OnceFunc(func() { // Initialize connection pool pool = NewPool( WithMaxConnections(10), WithTimeout(30*time.Second), ) }) func GetResource() (*Resource, error) { initPool() // Ensure the pool is initialized return pool.Get() }
π Performance Considerations
sync.Once has excellent performance. The overhead of the first call mainly comes from the mutex lock, and subsequent calls have almost zero overhead:
- First call: about 50-100ns (depending on lock competition)
- Subsequent calls: about 1-2ns (only atomic loading operation)
In high-concurrency scenarios, compared with other synchronization methods (such as mutex locks), it can significantly reduce performance loss.
π Summary
sync.Once solves the problem of single execution in a concurrent environment with an extremely simple design, and its core ideas are worth learning:
- Implement thread safety with minimal overhead
- Separate fast and slow paths to optimize performance
- Clear memory model guarantee
The three new functions added in Go 1.21 further improve its practicality, making the single execution logic more concise and robust.
Mastering the sync.Once family allows you to handle scenarios such as concurrent initialization and singleton patterns with ease, and write more elegant and efficient Go code.
Leapcell: The Best of Serverless Web Hosting
Finally, I recommend the best platform for deploying Go services: Leapcell
π Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
π Deploy Unlimited Projects for Free
Only pay for what you useβno requests, no charges.
β‘ Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
π Explore Our Documentation
πΉ Follow us on Twitter: @LeapcellHQ