Design Patterns in Go
Go’s philosophy favors simplicity over complexity. While traditional OOP patterns exist, Go often provides more idiomatic solutions using composition, interfaces, and first-class functions.Creational Patterns
Factory Pattern
Copy
// Product interface
type Database interface {
Connect() error
Query(sql string) ([]Row, error)
Close() error
}
// Concrete products
type PostgresDB struct {
host string
port int
user string
password string
}
func (p *PostgresDB) Connect() error {
// PostgreSQL connection logic
return nil
}
func (p *PostgresDB) Query(sql string) ([]Row, error) {
// Query logic
return nil, nil
}
func (p *PostgresDB) Close() error {
return nil
}
type MySQLDB struct {
host string
port int
user string
password string
}
func (m *MySQLDB) Connect() error { return nil }
func (m *MySQLDB) Query(sql string) ([]Row, error) { return nil, nil }
func (m *MySQLDB) Close() error { return nil }
// Factory function
func NewDatabase(dbType string, config Config) (Database, error) {
switch dbType {
case "postgres":
return &PostgresDB{
host: config.Host,
port: config.Port,
user: config.User,
password: config.Password,
}, nil
case "mysql":
return &MySQLDB{
host: config.Host,
port: config.Port,
user: config.User,
password: config.Password,
}, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", dbType)
}
}
// Usage
db, err := NewDatabase("postgres", config)
if err != nil {
log.Fatal(err)
}
defer db.Close()
Builder Pattern
Copy
type Server struct {
host string
port int
timeout time.Duration
maxConns int
tls bool
certFile string
keyFile string
middleware []Middleware
}
type ServerBuilder struct {
server *Server
}
func NewServerBuilder() *ServerBuilder {
return &ServerBuilder{
server: &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
},
}
}
func (b *ServerBuilder) Host(host string) *ServerBuilder {
b.server.host = host
return b
}
func (b *ServerBuilder) Port(port int) *ServerBuilder {
b.server.port = port
return b
}
func (b *ServerBuilder) Timeout(timeout time.Duration) *ServerBuilder {
b.server.timeout = timeout
return b
}
func (b *ServerBuilder) MaxConnections(max int) *ServerBuilder {
b.server.maxConns = max
return b
}
func (b *ServerBuilder) WithTLS(certFile, keyFile string) *ServerBuilder {
b.server.tls = true
b.server.certFile = certFile
b.server.keyFile = keyFile
return b
}
func (b *ServerBuilder) WithMiddleware(mw ...Middleware) *ServerBuilder {
b.server.middleware = append(b.server.middleware, mw...)
return b
}
func (b *ServerBuilder) Build() (*Server, error) {
if b.server.tls && (b.server.certFile == "" || b.server.keyFile == "") {
return nil, errors.New("TLS requires cert and key files")
}
return b.server, nil
}
// Usage
server, err := NewServerBuilder().
Host("0.0.0.0").
Port(443).
Timeout(60 * time.Second).
WithTLS("cert.pem", "key.pem").
WithMiddleware(loggingMiddleware, authMiddleware).
Build()
Functional Options Pattern (Idiomatic Go)
Copy
type Server struct {
host string
port int
timeout time.Duration
maxConns int
tls *TLSConfig
middleware []Middleware
}
type TLSConfig struct {
CertFile string
KeyFile string
}
// Option is a function that configures the server
type Option func(*Server) error
func WithHost(host string) Option {
return func(s *Server) error {
s.host = host
return nil
}
}
func WithPort(port int) Option {
return func(s *Server) error {
if port < 1 || port > 65535 {
return errors.New("invalid port")
}
s.port = port
return nil
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) error {
s.timeout = timeout
return nil
}
}
func WithTLS(certFile, keyFile string) Option {
return func(s *Server) error {
if certFile == "" || keyFile == "" {
return errors.New("cert and key files required")
}
s.tls = &TLSConfig{CertFile: certFile, KeyFile: keyFile}
return nil
}
}
func WithMiddleware(mw ...Middleware) Option {
return func(s *Server) error {
s.middleware = append(s.middleware, mw...)
return nil
}
}
// Constructor with options
func NewServer(opts ...Option) (*Server, error) {
// Default values
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
}
// Apply options
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
// Usage
server, err := NewServer(
WithHost("0.0.0.0"),
WithPort(443),
WithTimeout(60 * time.Second),
WithTLS("cert.pem", "key.pem"),
WithMiddleware(loggingMiddleware),
)
Singleton Pattern
Copy
// Using sync.Once (thread-safe)
type Database struct {
connection *sql.DB
}
var (
dbInstance *Database
dbOnce sync.Once
)
func GetDatabase() *Database {
dbOnce.Do(func() {
conn, err := sql.Open("postgres", connectionString)
if err != nil {
panic(err)
}
dbInstance = &Database{connection: conn}
})
return dbInstance
}
// Alternative: Package-level initialization
var db = mustInitDB()
func mustInitDB() *sql.DB {
db, err := sql.Open("postgres", connectionString)
if err != nil {
panic(err)
}
return db
}
Structural Patterns
Adapter Pattern
Copy
// Target interface (what the client expects)
type PaymentProcessor interface {
ProcessPayment(amount float64, currency string) error
}
// Adaptee (third-party SDK with different interface)
type StripeSDK struct{}
func (s *StripeSDK) Charge(amountCents int64, cur string, source string) (*StripeCharge, error) {
// Stripe-specific logic
return &StripeCharge{ID: "ch_123"}, nil
}
// Adapter
type StripeAdapter struct {
sdk *StripeSDK
source string
}
func NewStripeAdapter(apiKey string) *StripeAdapter {
return &StripeAdapter{
sdk: &StripeSDK{},
source: "tok_default",
}
}
func (a *StripeAdapter) ProcessPayment(amount float64, currency string) error {
amountCents := int64(amount * 100)
_, err := a.sdk.Charge(amountCents, currency, a.source)
return err
}
// Usage - client code works with PaymentProcessor interface
func checkout(processor PaymentProcessor, total float64) error {
return processor.ProcessPayment(total, "USD")
}
func main() {
stripe := NewStripeAdapter("sk_test_xxx")
checkout(stripe, 99.99)
}
Decorator Pattern
Copy
// Component interface
type Handler interface {
Handle(ctx context.Context, request *Request) (*Response, error)
}
// Concrete component
type UserHandler struct{}
func (h *UserHandler) Handle(ctx context.Context, request *Request) (*Response, error) {
return &Response{Data: "user data"}, nil
}
// Decorators
type LoggingDecorator struct {
handler Handler
logger *slog.Logger
}
func WithLogging(h Handler, logger *slog.Logger) Handler {
return &LoggingDecorator{handler: h, logger: logger}
}
func (d *LoggingDecorator) Handle(ctx context.Context, request *Request) (*Response, error) {
start := time.Now()
d.logger.Info("Handling request", "path", request.Path)
resp, err := d.handler.Handle(ctx, request)
d.logger.Info("Request completed",
"path", request.Path,
"duration", time.Since(start),
"error", err,
)
return resp, err
}
type CachingDecorator struct {
handler Handler
cache Cache
ttl time.Duration
}
func WithCaching(h Handler, cache Cache, ttl time.Duration) Handler {
return &CachingDecorator{handler: h, cache: cache, ttl: ttl}
}
func (d *CachingDecorator) Handle(ctx context.Context, request *Request) (*Response, error) {
key := request.CacheKey()
if cached, found := d.cache.Get(key); found {
return cached.(*Response), nil
}
resp, err := d.handler.Handle(ctx, request)
if err == nil {
d.cache.Set(key, resp, d.ttl)
}
return resp, err
}
// Usage - decorators can be stacked
handler := &UserHandler{}
handler = WithLogging(handler, logger)
handler = WithCaching(handler, cache, time.Minute)
Middleware Pattern (Go Idiomatic Decorator)
Copy
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func Logging(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("Request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
})
}
}
func Auth(validator TokenValidator) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !validator.Validate(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
// Usage
handler := Chain(
myHandler,
Logging(logger),
Auth(tokenValidator),
RateLimit(100),
)
Composite Pattern
Copy
// Component interface
type FileSystem interface {
Name() string
Size() int64
Print(indent string)
}
// Leaf
type File struct {
name string
size int64
}
func (f *File) Name() string { return f.name }
func (f *File) Size() int64 { return f.size }
func (f *File) Print(indent string) {
fmt.Printf("%s- %s (%d bytes)\n", indent, f.name, f.size)
}
// Composite
type Directory struct {
name string
children []FileSystem
}
func (d *Directory) Name() string { return d.name }
func (d *Directory) Size() int64 {
var total int64
for _, child := range d.children {
total += child.Size()
}
return total
}
func (d *Directory) Add(child FileSystem) {
d.children = append(d.children, child)
}
func (d *Directory) Print(indent string) {
fmt.Printf("%s+ %s/\n", indent, d.name)
for _, child := range d.children {
child.Print(indent + " ")
}
}
// Usage
root := &Directory{name: "root"}
src := &Directory{name: "src"}
src.Add(&File{name: "main.go", size: 1024})
src.Add(&File{name: "utils.go", size: 512})
root.Add(src)
root.Add(&File{name: "README.md", size: 256})
root.Print("")
// Output:
// + root/
// + src/
// - main.go (1024 bytes)
// - utils.go (512 bytes)
// - README.md (256 bytes)
Behavioral Patterns
Strategy Pattern
Copy
// Strategy interface
type CompressionStrategy interface {
Compress(data []byte) ([]byte, error)
Decompress(data []byte) ([]byte, error)
}
// Concrete strategies
type GzipStrategy struct{}
func (g *GzipStrategy) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
_, err := w.Write(data)
if err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
func (g *GzipStrategy) Decompress(data []byte) ([]byte, error) {
r, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
type ZlibStrategy struct{}
func (z *ZlibStrategy) Compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := zlib.NewWriter(&buf)
_, err := w.Write(data)
if err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
func (z *ZlibStrategy) Decompress(data []byte) ([]byte, error) {
r, err := zlib.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
// Context
type Compressor struct {
strategy CompressionStrategy
}
func NewCompressor(strategy CompressionStrategy) *Compressor {
return &Compressor{strategy: strategy}
}
func (c *Compressor) SetStrategy(strategy CompressionStrategy) {
c.strategy = strategy
}
func (c *Compressor) Compress(data []byte) ([]byte, error) {
return c.strategy.Compress(data)
}
// Usage
compressor := NewCompressor(&GzipStrategy{})
compressed, _ := compressor.Compress(data)
// Switch strategy at runtime
compressor.SetStrategy(&ZlibStrategy{})
compressed, _ = compressor.Compress(data)
Strategy with Functions (Go Idiomatic)
Copy
type Hasher func(data []byte) []byte
func MD5Hash(data []byte) []byte {
hash := md5.Sum(data)
return hash[:]
}
func SHA256Hash(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}
type Document struct {
content []byte
hasher Hasher
}
func NewDocument(content []byte, hasher Hasher) *Document {
return &Document{content: content, hasher: hasher}
}
func (d *Document) Hash() []byte {
return d.hasher(d.content)
}
// Usage
doc := NewDocument([]byte("hello"), SHA256Hash)
hash := doc.Hash()
Observer Pattern
Copy
// Observer interface
type Observer interface {
OnEvent(event Event)
}
type Event struct {
Type string
Payload interface{}
}
// Subject
type EventBus struct {
mu sync.RWMutex
observers map[string][]Observer
}
func NewEventBus() *EventBus {
return &EventBus{
observers: make(map[string][]Observer),
}
}
func (b *EventBus) Subscribe(eventType string, observer Observer) {
b.mu.Lock()
defer b.mu.Unlock()
b.observers[eventType] = append(b.observers[eventType], observer)
}
func (b *EventBus) Publish(event Event) {
b.mu.RLock()
observers := b.observers[event.Type]
b.mu.RUnlock()
for _, observer := range observers {
go observer.OnEvent(event) // Async notification
}
}
// Concrete observers
type EmailNotifier struct{}
func (e *EmailNotifier) OnEvent(event Event) {
if event.Type == "order.created" {
order := event.Payload.(*Order)
sendEmail(order.CustomerEmail, "Order Confirmation", "...")
}
}
type InventoryUpdater struct{}
func (i *InventoryUpdater) OnEvent(event Event) {
if event.Type == "order.created" {
order := event.Payload.(*Order)
updateInventory(order.Items)
}
}
// Usage
bus := NewEventBus()
bus.Subscribe("order.created", &EmailNotifier{})
bus.Subscribe("order.created", &InventoryUpdater{})
bus.Publish(Event{Type: "order.created", Payload: order})
Observer with Channels (Go Idiomatic)
Copy
type EventBus struct {
subscribers map[string][]chan Event
mu sync.RWMutex
}
func NewEventBus() *EventBus {
return &EventBus{
subscribers: make(map[string][]chan Event),
}
}
func (b *EventBus) Subscribe(eventType string) <-chan Event {
b.mu.Lock()
defer b.mu.Unlock()
ch := make(chan Event, 10)
b.subscribers[eventType] = append(b.subscribers[eventType], ch)
return ch
}
func (b *EventBus) Publish(event Event) {
b.mu.RLock()
defer b.mu.RUnlock()
for _, ch := range b.subscribers[event.Type] {
select {
case ch <- event:
default:
// Channel full, skip or log
}
}
}
// Usage
bus := NewEventBus()
orderEvents := bus.Subscribe("order.created")
go func() {
for event := range orderEvents {
order := event.Payload.(*Order)
processOrder(order)
}
}()
bus.Publish(Event{Type: "order.created", Payload: order})
State Pattern
Copy
// State interface
type OrderState interface {
Process(order *Order) error
Cancel(order *Order) error
Ship(order *Order) error
String() string
}
type Order struct {
ID string
Items []Item
State OrderState
}
func NewOrder(id string, items []Item) *Order {
order := &Order{ID: id, Items: items}
order.State = &PendingState{}
return order
}
func (o *Order) Process() error { return o.State.Process(o) }
func (o *Order) Cancel() error { return o.State.Cancel(o) }
func (o *Order) Ship() error { return o.State.Ship(o) }
// Concrete states
type PendingState struct{}
func (s *PendingState) String() string { return "pending" }
func (s *PendingState) Process(order *Order) error {
// Validate inventory, charge payment
order.State = &ProcessingState{}
return nil
}
func (s *PendingState) Cancel(order *Order) error {
order.State = &CancelledState{}
return nil
}
func (s *PendingState) Ship(order *Order) error {
return errors.New("cannot ship pending order")
}
type ProcessingState struct{}
func (s *ProcessingState) String() string { return "processing" }
func (s *ProcessingState) Process(order *Order) error {
return errors.New("already processing")
}
func (s *ProcessingState) Cancel(order *Order) error {
// Refund payment
order.State = &CancelledState{}
return nil
}
func (s *ProcessingState) Ship(order *Order) error {
order.State = &ShippedState{}
return nil
}
type ShippedState struct{}
func (s *ShippedState) String() string { return "shipped" }
func (s *ShippedState) Process(order *Order) error { return errors.New("already shipped") }
func (s *ShippedState) Cancel(order *Order) error { return errors.New("cannot cancel shipped order") }
func (s *ShippedState) Ship(order *Order) error { return errors.New("already shipped") }
type CancelledState struct{}
func (s *CancelledState) String() string { return "cancelled" }
func (s *CancelledState) Process(order *Order) error { return errors.New("order cancelled") }
func (s *CancelledState) Cancel(order *Order) error { return errors.New("already cancelled") }
func (s *CancelledState) Ship(order *Order) error { return errors.New("order cancelled") }
// Usage
order := NewOrder("123", items)
order.Process() // pending -> processing
order.Ship() // processing -> shipped
order.Cancel() // Error: cannot cancel shipped order
Go-Specific Patterns
Embedding (Composition over Inheritance)
Copy
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Embedding interfaces
type ReadWriter interface {
Reader
Writer
}
// Embedding structs
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // Embedded - Server "has-a" Logger and exposes its methods
port int
}
func NewServer(port int) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
port: port,
}
}
// Usage - Server has Log method via embedding
server := NewServer(8080)
server.Log("Starting...") // Calls embedded Logger.Log
Interface Segregation
Copy
// ❌ Bad: Large interface
type Repository interface {
Create(entity interface{}) error
Read(id string) (interface{}, error)
Update(entity interface{}) error
Delete(id string) error
List(filter Filter) ([]interface{}, error)
Count(filter Filter) (int, error)
BeginTransaction() (Transaction, error)
// ... many more methods
}
// ✅ Good: Small, focused interfaces
type Reader interface {
Read(id string) (interface{}, error)
}
type Writer interface {
Create(entity interface{}) error
Update(entity interface{}) error
}
type Deleter interface {
Delete(id string) error
}
type Lister interface {
List(filter Filter) ([]interface{}, error)
}
// Compose as needed
type ReadWriter interface {
Reader
Writer
}
type Repository interface {
Reader
Writer
Deleter
Lister
}
Interview Questions
How does Go implement polymorphism without inheritance?
How does Go implement polymorphism without inheritance?
Go uses interfaces and composition instead of inheritance:
- Interfaces define behavior contracts
- Types implicitly implement interfaces
- Embedding allows composition of behaviors
- Duck typing: if it implements the methods, it satisfies the interface
What's the functional options pattern and when should you use it?
What's the functional options pattern and when should you use it?
Functional options use variadic functions to configure objects. Use when:
- You have many optional configuration parameters
- You want clean, readable construction
- You need validation during configuration
- Default values should be easy to override
How do you implement dependency injection in Go?
How do you implement dependency injection in Go?
Go favors constructor injection:
- Pass dependencies as parameters to constructors
- Use interfaces for dependencies (testability)
- Avoid global variables and singletons
- Wire dependencies in main() or use DI containers sparingly
What's the difference between embedding and composition?
What's the difference between embedding and composition?
- Embedding: Type is embedded directly, methods are promoted
- Composition: Type is a field, access via field name
Summary
| Pattern | Go Implementation |
|---|---|
| Factory | Constructor functions |
| Builder | Fluent methods or functional options |
| Singleton | sync.Once or package-level init |
| Adapter | Wrapper struct implementing target interface |
| Decorator | Middleware pattern with function composition |
| Strategy | Interface + concrete implementations or functions |
| Observer | Channels or subscriber interface |
| State | Interface per state, context holds current state |