Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for sync.Once — Go's simple pattern for safe one-time execution.
Leapcell
Leapcell

Posted on

     

sync.Once — Go's simple pattern for safe one-time execution.

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

  1. Singleton pattern: Ensure that database connection pools, configuration loading, etc., are initialized only once
  2. Lazy loading: Load resources only when needed, and only once
  3. 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}}
Enter fullscreen modeExit fullscreen mode

The running result is always:

Only once
Enter fullscreen modeExit fullscreen mode

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()}}
Enter fullscreen modeExit fullscreen mode

Design Highlights:

  1. Double-Check Locking:

    • First check (without lock): quickly determine if it has been executed
    • Second check (after locking): ensure concurrent safety
  2. 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
  3. 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

  1. 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
Enter fullscreen modeExit fullscreen mode
  1. Avoid recursive calls: If once.Do(f) is called again in f, it will cause a deadlock

  2. 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()
Enter fullscreen modeExit fullscreen mode

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()}
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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()}
Enter fullscreen modeExit fullscreen mode

3. OnceValues: Supports returning two values

funcOnceValues[T1,T2any](ffunc()(T1,T2))func()(T1,T2)
Enter fullscreen modeExit fullscreen mode

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()}
Enter fullscreen modeExit fullscreen mode

🆚 Feature Comparison

FunctionCharacteristicsApplicable Scenarios
Once.DoBasic version, no return valueSimple initialization
OnceFuncWith panic handlingInitialization that needs error handling
OnceValueSupports returning a single valueCalculating and caching results
OnceValuesSupports returning two valuesOperations 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}
Enter fullscreen modeExit fullscreen mode

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...}
Enter fullscreen modeExit fullscreen mode

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()}
Enter fullscreen modeExit fullscreen mode

🚀 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:

  1. Implement thread safety with minimal overhead
  2. Separate fast and slow paths to optimize performance
  3. 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

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

leapcell.io: serverless web hosting / async task / redis
  • Location
    California
  • Joined

More fromLeapcell

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp