
Leapcell: The Best of Serverless Web Hosting
🔍 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 toguarantee 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:
packagemainimport("fmt""sync")funcmain(){varoncesync.OnceonceBody:=func(){fmt.Println("Only once")}// Start 10 goroutines to call concurrentlydone:=make(chanbool)fori:=0;i<10;i++{gofunc(){once.Do(onceBody)done<-true}()}// Wait for all goroutines to completefori:=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:
typeOncestruct{doneatomic.Uint32// Identifies whether the operation has been executedmMutex// Mutex lock}func(o*Once)Do(ffunc()){ifo.done.Load()==0{o.doSlow(f)// Slow path, allowing fast path inlining}}func(o*Once)doSlow(ffunc()){o.m.Lock()defero.m.Unlock()ifo.done.Load()==0{defero.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 examplevaroncesync.Onceonce2:=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
funcOnceFunc(ffunc())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:
packagemainimport("fmt""sync")funcmain(){// Create a function that executes only onceinitialize:=sync.OnceFunc(func(){fmt.Println("Initialization completed")})// Concurrent callsvarwgsync.WaitGroupfori:=0;i<5;i++{wg.Add(1)gofunc(){deferwg.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
funcOnceValue[Tany](ffunc()T)func()T
Suitable for scenarios where results need to be calculated and cached:
packagemainimport("fmt""sync")funcmain(){// Create a function that calculates only oncecalculate:=sync.OnceValue(func()int{fmt.Println("Start complex calculation")sum:=0fori:=0;i<1000000;i++{sum+=i}returnsum})// Multiple calls, only the first calculationvarwgsync.WaitGroupfori:=0;i<5;i++{wg.Add(1)gofunc(){deferwg.Done()fmt.Println("Result:",calculate())}()}wg.Wait()}
3. OnceValues: Supports returning two values
funcOnceValues[T1,T2any](ffunc()(T1,T2))func()(T1,T2)
Perfectly adapts to the Go function's idiom of returning (value, error):
packagemainimport("fmt""os""sync")funcmain(){// Read file only oncereadFile:=sync.OnceValues(func()([]byte,error){fmt.Println("Reading file")returnos.ReadFile("config.json")})// Concurrent readingvarwgsync.WaitGroupfori:=0;i<3;i++{wg.Add(1)gofunc(){deferwg.Done()data,err:=readFile()iferr!=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
typeDatabasestruct{// Database connection information}var(dbInstance*DatabasedbOncesync.Once)funcGetDB()*Database{dbOnce.Do(func(){// Initialize database connectiondbInstance=&Database{// Configuration information}})returndbInstance}
2. Lazy Loading of Configuration
typeConfigstruct{// Configuration items}varloadConfig=sync.OnceValue(func()*Config{// Load configuration from file or environment variablesdata,_:=os.ReadFile("config.yaml")varcfgConfig_=yaml.Unmarshal(data,&cfg)return&cfg})// Usagefuncmain(){cfg:=loadConfig()// Use configuration...}
3. Resource Pool Initialization
varinitPool=sync.OnceFunc(func(){// Initialize connection poolpool=NewPool(WithMaxConnections(10),WithTimeout(30*time.Second),)})funcGetResource()(*Resource,error){initPool()// Ensure the pool is initializedreturnpool.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.
🔹 Follow us on Twitter:@LeapcellHQ
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse