Right, sync.Once. This is the tool you reach for when you absolutely, positively must ensure something happens exactly one time, no matter how many goroutines are screaming for it. It’s the bouncer at the club of initialization, checking its internal list and saying, “Nope, you’ve already been in. Piss off.” Its API is beautifully simple, which is why we all love it and occasionally shoot ourselves in the foot with it.

Think about the classic problem: lazy initialization. You’ve got a variable, maybe a connection pool or some cached configuration, that you don’t want to set up until it’s actually needed. But once it’s needed, it might be needed by ten goroutines simultaneously. You can’t just use a plain old boolean flag and check if !initialized because in the chaos of concurrent execution, all ten goroutines might see initialized as false and all ten will trample each other trying to initialize it. That’s a data race and a surefire way to have a very bad, no-good day.

sync.Once solves this with a simple promise: Do(f func()) will run your function f, but only the very first time Do is called. All subsequent calls, no matter where they come from, will block until that first run is complete and then do nothing. It’s like a single-flight mechanism for initialization.

Here’s the canonical example: setting up a singleton. Please, for the love of all that is holy, don’t use this pattern for everything, but for idempotent, one-time setup, it’s perfect.

var (
    once     sync.Once
    instance *DatabaseConnection // This is our "singleton"
)

func getConnection() *DatabaseConnection {
    once.Do(func() {
        fmt.Println("Initializing the expensive database connection...")
        instance = &DatabaseConnection{
            // ... expensive setup logic here ...
        }
    })
    return instance
}

Now, you can call getConnection() from a hundred goroutines, and you’ll see the “Initializing…” print exactly once. The other 99 calls will just return the already-initialized instance. The magic is that Do isn’t just checking a flag; it uses atomic operations and mutexes under the hood to guarantee this behavior is safe across all CPU cores. You don’t have to think about the memory barriers or the lock—it just works.

The Inner Workings (Or, Why It’s Not Magic)

It’s not actually magic, of course. It’s just good, careful engineering. Internally, a sync.Once has a mutex and a uint32 (or similar) acting as an atomic flag. When you call Do, it performs a fast, atomic check of that flag. If it’s already set, it returns immediately. If not, it acquires the mutex, checks the flag again (this is the crucial double-check inside the mutex to prevent races), and if it’s still clear, it runs your function, sets the flag via another atomic store, and releases the mutex. This combination of atomic memory operations and a mutex is what makes it so robust. It’s a pattern you could write yourself, but you absolutely shouldn’t because these primitives are notoriously easy to get wrong. Just use sync.Once.

The One Big Gotcha: The Function Itself

Here’s the part that trips people up. The guarantee is that the function will be called once. It makes no guarantees about what your function does. If your function panics, sync.Once considers its job done. The flag is set. So, if you call Do again with the same once, your function will not run again, and you’ll be left with a nil or partially initialized singleton. This is a feature, not a bug—a panic during initialization is usually a permanent state. You can’t recover from it by trying again.

var once sync.Once

func riskyInit() {
    once.Do(func() {
        fmt.Println("About to do something stupid...")
        panic("I regret nothing and everything")
    })
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
        // Now try to call it again. It won't run.
        once.Do(func() {
            fmt.Println("This will never print.")
        })
        fmt.Println("We are now permanently hosed.")
    }()
    riskyInit()
}

The output will show the panic being recovered, but the second Do call does nothing. Your initialization failed. Game over. The lesson is simple: your Do function must be bulletproof. It should handle any potential panics internally if you want a chance to retry. Otherwise, you’ll need a different mechanism.

Using Multiple sync.Once Objects

A single sync.Once is tied to a single initialization action. If you have multiple, independent things to initialize, you need multiple sync.Once instances. This is a common pattern.

var (
    configOnce sync.Once
    cacheOnce  sync.Once
    config     *Config
    cache      *Cache
)

func GetConfig() *Config {
    configOnce.Do(loadConfig) // loadConfig is a func that sets `config`
    return config
}

func GetCache() *Cache {
    cacheOnce.Do(initCache) // initCache is a func that sets `cache`
    return cache
}

This keeps your initializations isolated and safe from each other. Trying to cram two different initialization routines into one sync.Once is a recipe for confusion and is a clear sign your code needs refactoring.

In short, sync.Once is one of the most reliable and useful tools in the sync package. It does one thing perfectly. Use it for any expensive, idempotent initialization that needs to be thread-safe. Just remember: write your init function like your program’s life depends on it, because if it fails, that part of your program is dead.