Skip to content

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.

go
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 ​

go
type Once struct {
    done atomic.Uint32
    m    Mutex
}

Its internal structure is very simple:

  1. done, an atomic value, used to indicate whether it has been executed
  2. m, 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 ​

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

  1. Directly load the atomic value, if already executed, return immediately
  2. If not executed, try to acquire the lock, here multiple goroutines may be competing for the mutex
  3. After the winner successfully acquires the lock, it still needs to check the done value again, because after you acquire the lock, someone else may have already completed the lock-acquire-execute-unlock sequence, and you were just awakened.
  4. Execute the target function
  5. Update the done value
  6. 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.

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

text
init once
done

Common Pitfalls ​

  • Once guards one function, not one call site. Use a separate Once value per initialisation.
  • A panic inside Do still 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.OnceValue wrap 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.

Golang by www.golangdev.cn · Adapted in part from Golang 中文学习文档 (MIT, © 寒江蓑笠翁)