Creational · Structural · Behavioral · Concurrency · Architecture — Every pattern explained with real-world Go code, diagrams, use cases, and production context.
sync.Once for
goroutine safety.The Singleton pattern restricts instantiation of a type to a single object. Every part of your
application accesses the same shared instance. In Go, the idiomatic implementation uses
sync.Once which guarantees the initializer runs exactly once even under concurrent goroutine
access — unlike many other languages that rely on double-checked locking.
Do(f func()) methodDo() executes the function; subsequent calls are no-opssync.Once, not init() or
var instance = &T{} alone
var once sync.Oncevar instance *TypeGetInstance() calls once.Do()Create the type that should have only one instance. Keep it unexported
(type dbPool struct) so external packages can't instantiate it directly.
Declare var once sync.Once and var instance *dbPool at package level — both
zero values, no allocation yet.
Exported function calls once.Do(func(){ instance = newDBPool() }). The Do
call is goroutine-safe — no mutex needed in your code.
Add methods to the struct. Callers use GetInstance().Query() — they get the singleton
without knowing it's one.
Return an interface from GetInstance() so callers can mock it in tests — critical for
testability.
db.GetInstance() to access the same PostgreSQL connection pool — never creating 50
separate pools. The singleton manages pool sizing, keeps connections warm, and handles reconnection —
transparently to every terminal handler.
When an object needs many configuration options — some required, some optional — a single constructor
becomes unwieldy. Builder solves this by using a separate Builder struct with setter methods
that each return the builder itself, enabling fluent method chaining:
NewQuery().Table("orders").Where("status=?","pending").Limit(10).Build().
Builder struct mirroring the target struct's fields*BuilderBuild() method validates and constructs the final objectBuild() — not during chainingBuild() — never silently ignored*Builder for chainingfunctional options pattern as a Go-idiomatic alternativeGo's idiomatic alternative: type Option func(*Server). Functions like
WithTimeout(t time.Duration) return Option closures applied at construction.
Used extensively in the Go standard library and popular packages (gRPC, zap).
NewNotification().To(userID).Via(SMS, Email).Message("Your order is ready!").Schedule(now.Add(5*time.Minute)).Build()
— each field independent, validated at Build() time before sending to the notification microservice.
Instead of &ConcreteType{}, callers use a factory function like
NewPaymentGateway("stripe") which returns a PaymentGateway interface. The caller
doesn't know — or care — whether they got a Stripe, PayPal, or Razorpay implementation. Adding new
implementations requires no changes to caller code.
You have a system expecting a Logger interface with Info(msg string), but the
third-party library you want to use has Log(level, msg string). The Adapter wraps the
third-party logger in a struct that implements your Logger interface — no changes to your
system, no changes to theirs. Plug and play.
Go's structural typing makes Adapter very natural. If a type has the right methods, it automatically
satisfies the interface — no implements keyword. This means many types are already
adapters. The io.Reader/io.Writer ecosystem is built entirely on this principle.
select { case ch <- e: default: }Go channels make Observer feel native. A channel IS a typed, buffered, goroutine-safe queue.
Each subscriber is just a goroutine reading from its channel. This eliminates explicit locking in many
patterns. The select statement enables non-blocking multi-topic subscription naturally.
bus.Publish("order.placed", evt) call.Your Sorter service needs to sort data — sometimes QuickSort (best average), sometimes
MergeSort (stable), sometimes RadixSort (integers only). Strategy wraps each algorithm behind a common
interface. At runtime, inject the right strategy based on data type, size, or user preference — no if/else
chains.
In Go, a Strategy can be as simple as a function type:
type PricingStrategy func(basePrice float64, quantity int) float64. Functions like
BulkDiscount, MemberDiscount, FlashSale all satisfy this type. This
is the most idiomatic Go approach — no interface boilerplate for simple strategies.
SetStrategy() method allows runtime swapExecute() delegates to the current strategyio package works entirely.You have a DataStore interface. You want to add caching to it — but only for certain
deployments, and you don't want to modify the core store. Wrap it:
CachedStore{inner: realStore}. The cached store checks cache first, falls through to inner on
miss. Stack more: LoggedStore{inner: CachedStore{inner: realStore}}. Infinite composability.
A{B{C{core}}}bufio.NewReader(r io.Reader) — buffered decoratorgzip.NewReader(r io.Reader) — decompression decoratorio.LimitReader(r, n) — size-limiting decoratorhttp.ResponseWriter — HTTP middleware is decoratortls.Client(conn) — TLS wrapping decoratorWithout a pool: 10,000 incoming requests → 10,000 goroutines → OOM crash. With a pool: 10,000 requests → 100 workers process them sequentially via channel queue. Workers block waiting for jobs when idle — zero CPU waste. Max goroutines = pool size. Memory usage is bounded and predictable.
range jobs — blocks when idle, wakes on new jobsync.WaitGroup to know when all workers are donesync.WaitGroup — signal when all workers donefunc stage(in <-chan T) <-chan UChannels provide natural backpressure. If stage 3 is slow, stage 2's output channel fills up, blocking stage 2, which signals stage 1 to slow down. The pipeline self-regulates — fast producers don't overwhelm slow consumers. This is a critical property for production data systems.
ctx.Done()defer close(out) at the start of each stage goroutinesync.WaitGroup to close merged channel when all doneselect to get fastestA hotel booking app queries Booking.com, Expedia, and Airbnb simultaneously (fan-out → 3 goroutines, 3 HTTP calls). Results fan-in to a merged channel. First response shows immediately, others fill in — total latency = slowest source, not sum of all sources. This can cut latency by 3x.
repo.FindByID().Instead of scattering SQL queries across your codebase, all data access for a domain entity is
centralized in a Repository. The service layer depends on a UserRepository interface
— not on PostgreSQL. Swap PostgreSQL for MongoDB? Write a new implementation, inject it. Service code
unchanged.
UserRepository — domain methods (Find, Save, Delete)*sql.Tx or use Unit of Work patternThe killer feature: your service tests use an InMemoryUserRepository — no database required.
Tests run in milliseconds, not seconds. CI/CD pipelines need no database container. Integration tests run
only for the repository implementation itself.
http.Handler with pre/post processing logic in a
chainable, composable way. Each middleware is a function that takes the next handler and returns a new
handler — creating an onion-layered request processing pipeline.In Go, a middleware is simply a function: func(http.Handler) http.Handler. It wraps any
handler, adding behavior before and/or after the inner handler runs. Chain them together:
Logger(Auth(RateLimit(actualHandler))). The request passes through each layer, response
travels back through them in reverse order.
next.ServeHTTP(w, r) — passes to inner handlernext.ServeHTTP() or terminatecontext.WithValue()ResponseWriter after calling nexthttp.ResponseWriter wrappers to capture status codes| Criteria | 🔒 Singleton | 🏗️ Builder | 🏭 Factory | 🔌 Adapter | 👁️ Observer | ♟️ Strategy | 🎁 Decorator | ⚙️ Worker Pool | 🔗 Pipeline | 📡 Fan-Out/In | 🗄️ Repository | 🔧 Middleware |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Category | Creational | Creational | Creational | Structural | Behavioral | Behavioral | Structural | Concurrency | Concurrency | Concurrency | Architectural | Behavioral |
| Go-Specific | ~ (sync.Once) | ✗ GoF classic | ✗ GoF classic | ✓ implicit iface | ✓ channels | ✓ func types | ✗ GoF classic | ✓✓ goroutines | ✓✓ goroutines | ✓✓ goroutines | ✗ DDD classic | ✓✓ http.Handler |
| Complexity | Low | Medium | Low-Medium | Low | Medium | Low | Low | Medium | Medium-High | Medium-High | Medium | Low |
| Boilerplate | Very Low | High | Medium | Low | Medium | Low | Low | Medium | Medium | Medium | High | Very Low |
| Testability | ✗ Global state | ✓✓ Immutable obj | ✓✓ Mock impl | ✓✓ Mock adapter | ✓ Test channels | ✓✓ Inject strategy | ✓✓ Stack mock | ✓ Channel test | ✓ Channel test | ✓ Channel test | ✓✓ InMemory impl | ✓✓ Wrap test handler |
| Performance Impact | Near-zero (after once) | None (object creation) | None (interface call) | One indirection | Channel overhead | None (interface call) | One indirection/layer | High gain (bounded) | High gain (parallel) | High gain (parallel) | Negligible | Negligible per middleware |
| Goroutine-safe | ✓ sync.Once | ✓ immutable result | ✓ if impl is safe | ✓ if inner safe | ✓ RWMutex | ✓ stateless | ~ if inner safe | ✓✓ channels | ✓✓ channels | ✓✓ channels | ~ tx handling | ✓ per-request |
| Open/Closed Principle | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ~ | ~ | ~ | ✓✓ | ✓✓ |
| Decoupling Level | Low (global) | High (immutable obj) | Very High | Very High | High | Very High | High | High | High | High | Very High | High |
| When to Avoid | If DI is possible | Simple <4 fields | Single impl only | Same interface already | Simple callbacks ok | Only 1 algorithm | No extra behavior | Few goroutines ok | Single-threaded ok | Sequential is fine | Simple CRUD apps | Single handler apps |
| Industry Usage | ★★★★★ | ★★★★☆ | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★★★ | ★★★★★ |
| Best Project Type | Any — shared resources | Complex configs | Multi-provider systems | 3rd party integration | Event-driven systems | Multi-algorithm contexts | Cross-cutting behavior | Bulk/async processing | ETL, data streams | Parallel API calls | Any multi-DB system | Any HTTP API |
| Go Stdlib Example | sync.Once | strings.Builder | http.NewRequest | io.Writer wrappers | signal.Notify chan | sort.Interface | bufio, gzip readers | sync.WaitGroup | io.Pipe | reflect.Select | database/sql | net/http Handler |