{} The Go Reference

Creational pattern · Gang of Four · Beginner

Singleton

Ensure a type has only one instance, and provide a single, well-defined point of access to it.

Also known as — Single instance

Creational Beginner ⏱ 5 min read Complete

🚗 Analogy

A moving car has exactly one driver at the wheel. Everyone inside — and everything the car does — answers to that single driver, and there’s one well-known seat to reach them. You can’t have two people steering at once.

The problem

Some things should exist once per program: a configuration loaded from the environment, a database connection pool, a shared logger. If every caller created its own, you’d get inconsistent config, exhausted connections, or interleaved log files.

You need two guarantees: only one instance is ever created, and everyone can reach that same instance through one access point. And in Go specifically: initialization must be safe when many goroutines ask for the instance at once.

Structure

classDiagram
class Singleton {
  -instance Singleton
  +GetInstance() Singleton
  +SomeMethod()
}
note for Singleton "GetInstance() always returns the same instance. The constructor is unexported."

Callers never construct the type directly — they go through a single accessor that hands back the one shared value.

How it works

sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant S as GetInstance / sync.Once
G1->>S: GetInstance()
S->>S: once.Do creates instance (runs ONCE)
S-->>G1: *instance
G2->>S: GetInstance()
S-->>G2: *instance (same pointer, no re-init)

Idiomatic Go

Go’s standard library hands you the perfect tool: sync.Once. Its Do method runs a function exactly once, no matter how many goroutines call it concurrently — and it’s faster than locking on every access.

package config

import "sync"

type Config struct {
	DSN   string
	Debug bool
}

var (
	instance *Config
	once     sync.Once
)

// Get returns the one shared Config, initializing it on first use.
// Safe to call from many goroutines at once.
func Get() *Config {
	once.Do(func() {
		instance = &Config{DSN: "postgres://localhost/app"}
	})
	return instance
}

Try it — this version spins up five goroutines that all race to initialize, then proves they got the same instance. Edit it and hit Run:

singleton.go — editable & runnable
package main

import (
"fmt"
"sync"
)

type Config struct {
DSN   string
Debug bool
}

var (
instance *Config
once     sync.Once
)

func Get() *Config {
once.Do(func() {
	fmt.Println("initializing config (this prints once)")
	instance = &Config{DSN: "postgres://localhost/app"}
})
return instance
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
	wg.Add(1)
	go func() { defer wg.Done(); _ = Get() }()
}
wg.Wait()

a, b := Get(), Get()
fmt.Printf("same instance: %v\n", a == b)
fmt.Println("DSN:", a.DSN)
}

💡 Even simpler: eager init

If the instance is cheap and always needed, skip the laziness — a package-level variable initialized at declaration is a singleton, and Go guarantees package initialization runs once before main.

var instance = &Config{DSN: "postgres://localhost/app"} // created once, at startup
func Get() *Config { return instance }

A parameterized variation

One variation passes the *sync.Once in as a parameter and even recovers from panics during construction — a nice way to see the mechanics laid bare. In production you’d usually keep the sync.Once as an unexported package-level variable instead.

Under the hood: why sync.Once beats double-checked locking

In Java, the classic lazy singleton drove people to double-checked locking — check, lock, check again, with a volatile field — a notoriously easy thing to get subtly wrong. Go sidesteps the whole saga: sync.Once does the careful work for you. Internally it keeps a done flag read with an atomic load on the fast path, so after the first call Do is a single cheap atomic check with no mutex — and the slow path takes a lock only until initialization finishes. You get correct, race-free lazy init without ever writing the locking dance yourself (and go test -race will confirm it).

The deeper lesson from the foundations: the pattern’s value is the single access point and the one-instance guarantee, not the locking. Go gives you the locking for free, which is exactly why a bare nil check is both unnecessary and unsafe.

In the standard library

  • sync.Once itself is the canonical building block.
  • http.DefaultClient, http.DefaultServeMux are package-level singletons you use every day.
  • Go’s package initialization (init() + package-level vars) is a language-level singleton guarantee.

Pitfalls

⚠️ Singleton is global state wearing a tie

The pattern’s biggest danger isn’t concurrency — sync.Once handles that. It’s that a singleton is hidden, shared, mutable state. It couples every caller to one concrete value, makes unit tests share state across runs, and resists mocking. Before reaching for it, ask: “could I just pass this in as a dependency?” Often the answer is yes, and your code gets more testable for it.

When to use it — and when not

✅ Reach for it when

  • There must be exactly one instance — a process-wide config, a connection pool, a logger, a metrics registry.
  • That single instance needs a clear, shared access point across the program.
  • Lazy, one-time initialization needs to be safe under concurrency.

⛔ Think twice when

  • You're really just reaching for a global variable — singletons hide global mutable state and make tests order-dependent.
  • You need to swap the implementation in tests; a singleton is hard to mock. Prefer passing a dependency in.
  • Multiple configurations might be needed later — a singleton bakes 'exactly one' into your architecture.

Check your understanding

Score: 0 / 5

1. What is the idiomatic, concurrency-safe way to build a lazy singleton in Go?

sync.Once guarantees the init function runs exactly once, even under concurrent calls, with no race — and no lock on the hot path after the first call.

2. What's the main design risk of the Singleton pattern?

A singleton is global state in disguise. It couples callers to a concrete instance and makes tests share state — often better to inject the dependency.

3. Why is a bare `if instance == nil { instance = ... }` unsafe?

Without synchronization, concurrent callers race on the check-and-set, possibly creating two instances and tripping the race detector.

4. When is a package-level `var instance = &Config{...}` (eager singleton) initialized?

Go initializes package-level variables once, in dependency order, before main starts and before any goroutine can run. So an eager singleton needs no sync.Once at all — reach for sync.Once only when initialization must be lazy (expensive, or depends on runtime state).

5. Your code calls config.Get() everywhere. How do you make it testable?

The singleton's testability tax comes from hidden global coupling. The fix is dependency injection: have functions take what they need as an argument (often a small interface). The 'singleton' can still construct the default and pass it in at main — tests inject their own.

Comments

Sign in with GitHub to join the discussion.