What It Is
The Singleton pattern makes sure a type has exactly one instance in the whole program, and gives everyone the same access point to it. Typical singletons: a configuration, a connection pool, a logger.
Go's tool for this is sync.Once: the initialization code inside once.Do() runs exactly once, even when many goroutines ask at the same time.
Example: One Config for Everyone
The config is loaded on the first GetConfig() call and never again. Every caller receives the same pointer.
Load once with sync.Once
The "loading config" line prints only once, although GetConfig() is called twice — and a == b proves both callers hold the same instance.
Output: Go program and captured output
package main
import (
"fmt"
"sync"
)
type Config struct {
AppName string
}
var (
once sync.Once
instance *Config
)
func GetConfig() *Config {
once.Do(func() {
fmt.Println("loading config (happens once)")
instance = &Config{AppName: "PromptCookbook"}
})
return instance
}
func main() {
a := GetConfig()
b := GetConfig()
fmt.Println("app:", a.AppName)
fmt.Println("same instance:", a == b)
}
When to Use It
Use it when there really must be one of something: one loaded configuration, one connection pool, one cache — and creating it is expensive or duplicates would conflict.
Skip it when you can simply create the value in main() and pass it to whoever needs it. Passing dependencies openly is clearer than global access — the singleton is the last resort, not the first.
Pros and Cons
Pros: guaranteed single instance; lazy creation on first use; safe with many goroutines via sync.Once.
Cons: it is a global in disguise — hidden dependencies make code harder to follow; tests cannot easily swap it for a fake; overuse leads to tangled programs.