Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
160: sync.Once for One-Time Initialization
In a real-world Go application, you'll often run into a situation where you have a heavy resource—like a database connection pool, a configuration object, or a complex cache—that you only want to initialize once. You don't want to do it at the very start of main() because it might slow down your startup time or fail if a dependency isn't ready yet. You want "lazy initialization": create it the first time someone actually asks for it, and then reuse that same instance forever.
The Goal: A Lazy-Loaded Config
Let's build a simple configuration loader. Imagine we have a Config struct that reads from a file. Reading from a disk is slow, so we definitely don't want to do it every time we need a setting. I'll start by defining our struct and a global variable to hold the singleton instance.
type Config struct {
APIKey string
Port int
}
var instance *Config
The Naive Approach (and where I messed up)
When I first started writing these types of patterns, I thought a simple if check was enough. It seems logical: "If the instance is nil, create it. Otherwise, just return it." Here is how I wrote it originally:
func GetConfig() *Config {
if instance == nil {
fmt.Println("Loading config from disk...")
instance = &Config{
APIKey: "secret-123",
Port: 8080,
}
}
return instance
}
This works fine in a single-threaded program. But we're writing Go, and we're almost always dealing with goroutines. If two goroutines call GetConfig() at the exact same millisecond, they both might see instance == nil as true. They'll both hit the disk, and they'll both try to assign a value to instance. That's a classic data race. If you run this with the -race flag, Go will scream at you.
Solving it with sync.Once
I could wrap that check in a sync.Mutex, but that means every single time we call GetConfig() for the rest of the program's life, we're paying the performance penalty of locking and unlocking a mutex just to check a pointer. That's overkill.
This is exactly why sync.Once exists. It's a small, highly optimized object that guarantees a function is executed exactly once, regardless of how many goroutines call it. Here is the professional way to handle this:
import (
"fmt"
"sync"
)
type Config struct {
APIKey string
Port int
}
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("Loading config from disk...")
instance = &Config{
APIKey: "secret-123",
Port: 8080,
}
})
return instance
}
Notice that once.Do takes a function. The first goroutine to hit that line will execute the function. Any other goroutines that arrive while the first one is still running will block—they'll wait patiently until the first one finishes. Once the function returns, every subsequent call to once.Do will simply return immediately without doing anything. It's clean, thread-safe, and efficient.
A quick word on init()
You might be wondering, "Why not just use an init() function?" You can, but init() runs as soon as the package is imported. If your configuration loading fails (e.g., the file is missing), init() will either cause a panic or leave your app in a weird state before main() even starts. By using sync.Once inside a getter function, you control exactly when the initialization happens, and you can handle errors more gracefully if you decide to return an error from your getter.
📋 Practical Task
Exercise: Implement a Thread-Safe Database Connection Singleton
You are building a system where multiple goroutines need access to a shared database connection object. However, creating the connection is expensive and should only happen once.
Your Task:
- Create a struct named
DBConnectionwith a fieldConnectionID(string). - Declare a global variable
dbInstanceof type*DBConnection. - Declare a
sync.Oncevariable. - Implement a function
GetDBConnection() *DBConnectionthat usessync.Onceto initialize thedbInstancewith aConnectionIDof "DB-PROD-01" only on the first call. - In your
mainfunction, launch 10 concurrent goroutines that all callGetDBConnection()and print the resultingConnectionIDto prove that they are all sharing the exact same instance.
There are no comments for now.