# Go Design Patterns: Essential Patterns and Interview Questions for Go Developers > Master Go design patterns including Functional Options, Strategy, Factory, and Observer. Practical code examples, idiomatic best practices, and common interview questions for Go developers. - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, best-practices, interview, software-architecture - Reading time: 10 min --- Go design patterns differ fundamentally from their counterparts in object-oriented languages. Without classes or inheritance, Go relies on composition, interfaces, and first-class functions to achieve the same structural flexibility—often with less ceremony and more clarity. > **Idiomatic Go Principle** > > Go favors composition over inheritance. Every classic design pattern must be adapted to work with interfaces, struct embedding, and functions as first-class citizens rather than class hierarchies. ## The Functional Options Pattern for Flexible Configuration The Functional Options pattern solves a common problem in Go: constructing objects with many optional parameters. Unlike languages with method overloading or default arguments, Go has neither. Builder patterns work but feel verbose. Functional Options provide a clean, extensible API that remains backward-compatible as requirements grow. This pattern, [popularized by Dave Cheney](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) and now standard across the Go ecosystem, uses variadic function arguments to configure a struct. ```go // server.go package server import ( "time" "log/slog" ) // Server holds the HTTP server configuration. type Server struct { host string port int timeout time.Duration maxConns int logger *slog.Logger } // Option defines a functional option for Server. type Option func(*Server) // WithPort sets the server port. func WithPort(port int) Option { return func(s *Server) { s.port = port } } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } // WithMaxConns sets the maximum concurrent connections. func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } // New creates a Server with sensible defaults and applies options. func New(host string, opts ...Option) *Server { srv := &Server{ host: host, port: 8080, // default port timeout: 30 * time.Second, // default timeout maxConns: 100, // default max connections logger: slog.Default(), } for _, opt := range opts { opt(srv) } return srv } ``` The caller specifies only what differs from defaults. Adding a new option never breaks existing call sites. This pattern appears in production libraries like `google.golang.org/grpc` and `go.uber.org/zap`. ## Strategy Pattern Through Interface Satisfaction The Strategy pattern encapsulates interchangeable algorithms behind a common interface. In Go, this maps directly to interface-based polymorphism—no abstract classes or inheritance chains required. A payment processing system demonstrates the pattern. Each payment method implements the same interface, and the checkout service selects the strategy at runtime. ```go // payment.go package payment import "fmt" // Processor defines the strategy interface. type Processor interface { Pay(amount float64) (string, error) } // CreditCard implements Processor for card payments. type CreditCard struct { CardNumber string Expiry string } func (c *CreditCard) Pay(amount float64) (string, error) { // Charge the card via payment gateway return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil } // BankTransfer implements Processor for wire transfers. type BankTransfer struct { IBAN string } func (b *BankTransfer) Pay(amount float64) (string, error) { return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil } // Checkout processes a payment using the given strategy. func Checkout(p Processor, amount float64) error { receipt, err := p.Pay(amount) if err != nil { return fmt.Errorf("payment failed: %w", err) } fmt.Println(receipt) return nil } ``` Go interfaces are satisfied implicitly—no `implements` keyword. Any type with a `Pay(float64) (string, error)` method qualifies as a `Processor`. This keeps coupling low and makes testing straightforward: pass a mock `Processor` that returns predictable results. ## Factory Functions and Constructor Patterns in Go Go has no constructors. The idiomatic replacement is a factory function, typically named `New` or `NewXxx`, that returns an initialized struct. Factory functions enforce invariants that zero-value initialization cannot guarantee. The distinction matters for interview preparation: Go developers should recognize when a factory function is necessary versus when the [zero value is useful by default](https://go.dev/blog/organizing-go-code). ```go // connpool.go package pool import ( "errors" "sync" ) // ConnPool manages a pool of reusable connections. type ConnPool struct { mu sync.Mutex conns []Conn maxSize int } // Conn represents a database connection. type Conn struct { ID int Active bool } // NewConnPool validates parameters and returns an initialized pool. func NewConnPool(maxSize int) (*ConnPool, error) { if maxSize <= 0 { return nil, errors.New("pool: maxSize must be positive") } return &ConnPool{ conns: make([]Conn, 0, maxSize), maxSize: maxSize, }, nil } // Acquire returns a connection from the pool. func (p *ConnPool) Acquire() (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() for i := range p.conns { if !p.conns[i].Active { p.conns[i].Active = true return &p.conns[i], nil } } if len(p.conns) >= p.maxSize { return nil, errors.New("pool: no available connections") } c := Conn{ID: len(p.conns) + 1, Active: true} p.conns = append(p.conns, c) return &p.conns[len(p.conns)-1], nil } ``` The factory function `NewConnPool` validates the `maxSize` parameter and pre-allocates the slice. Direct struct initialization (`&ConnPool{}`) would skip validation, potentially leading to runtime panics. This pattern extends naturally: combine it with Functional Options for more complex configuration scenarios. ## Observer Pattern with Channels and Goroutines The Observer pattern notifies multiple subscribers when state changes. In Java or C#, this involves event listeners and callback registration. Go offers a more idiomatic path: channels. Each subscriber receives events through its own channel, and goroutines handle delivery concurrently. This approach leverages Go's [concurrency primitives](/blog/go/go-concurrency-goroutines-channels) directly, avoiding callback spaghetti. ```go // eventbus.go package events import "sync" // Event carries a topic and payload. type Event struct { Topic string Payload any } // Bus manages subscriptions and event dispatch. type Bus struct { mu sync.RWMutex subscribers map[string][]chan Event } // NewBus creates an event bus. func NewBus() *Bus { return &Bus{ subscribers: make(map[string][]chan Event), } } // Subscribe returns a channel that receives events for a topic. func (b *Bus) Subscribe(topic string) <-chan Event { ch := make(chan Event, 16) // buffered to avoid blocking publisher b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish sends an event to all subscribers of the topic. func (b *Bus) Publish(topic string, payload any) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- Event{Topic: topic, Payload: payload}: default: // subscriber too slow, drop event } } } ``` The `select` with a `default` case prevents a slow subscriber from blocking the entire bus. In production systems, adding a context-based cancellation mechanism and an `Unsubscribe` method would complete the implementation. ## Middleware Pattern for HTTP Request Pipelines Middleware chains are the backbone of Go HTTP servers. The pattern wraps an `http.Handler` with additional behavior—logging, authentication, rate limiting—without modifying the handler itself. The standard library's `http.Handler` interface makes this trivially composable. ```go // middleware.go package middleware import ( "log/slog" "net/http" "time" ) // Logging records request duration and status. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.status, "duration", time.Since(start), ) }) } // statusWriter captures the HTTP status code. type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // Chain applies middleware in order: first listed = outermost. func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } ``` The `Chain` function applies middleware in declaration order. Each middleware wraps the next, forming a pipeline. This pattern is identical in [popular routers like chi](https://github.com/go-chi/chi), which accepts `func(http.Handler) http.Handler` as middleware. ## Composition Over Inheritance with Struct Embedding Go does not support inheritance. Instead, struct embedding promotes methods from an inner type to the outer type, achieving code reuse without tight coupling. This is not inheritance—the embedded type has no knowledge of the embedding type, and there is no virtual dispatch. Understanding this distinction is critical for [Go interview questions](/technologies/go/interview-questions/concurrency-patterns). Candidates who conflate embedding with inheritance reveal a shallow understanding of Go's type system. ```go // models.go package models import "time" // Timestamps provides common audit fields. type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } // User embeds Timestamps to gain CreatedAt/UpdatedAt. type User struct { Timestamps ID int Email string } // Order also embeds Timestamps. type Order struct { Timestamps ID int UserID int Total float64 } ``` Both `User` and `Order` now expose `CreatedAt` and `UpdatedAt` fields directly. Any methods defined on `Timestamps` are also promoted. The key rule: embedding provides delegation, not substitution. A `User` is not a `Timestamps`—it has a `Timestamps`. ## Interview Questions on Go Design Patterns Design pattern questions in Go interviews test whether a candidate can adapt classical patterns to Go's type system. The following questions appear frequently across technical screens and on-site rounds. **Q: When should a factory function return an error instead of panicking?** Factory functions should return an error whenever validation depends on runtime input (user-provided config, environment variables, external data). Panicking is reserved for programmer errors—situations that indicate a bug, such as passing a nil pointer where the API contract forbids it. The standard library follows this convention: `os.Open` returns an error, while `regexp.MustCompile` panics because it expects a compile-time-constant pattern. **Q: How does the Functional Options pattern improve API evolution?** Adding a new `WithXxx` function is a non-breaking change. Existing callers continue to work without modification. This contrasts with config structs, where adding a required field breaks all call sites, or with positional parameters, where reordering arguments introduces bugs. **Q: What makes Go interfaces different from Java or C# interfaces?** Go interfaces are satisfied implicitly. A type implements an interface simply by having the required methods—no `implements` declaration needed. This enables the "accept interfaces, return structs" principle: define narrow interfaces at the call site, not at the implementation site. The result is [lower coupling and higher testability](/technologies/go/interview-questions/testing). > **Interview Pattern** > > Interviewers often ask candidates to refactor a concrete dependency into an interface. The test: can the candidate identify the minimal method set needed and extract it at the consumer side rather than the producer side? **Q: How does channel-based Observer differ from callback-based Observer?** Channels decouple the publisher and subscriber in both time and space. The publisher does not hold references to subscriber functions—it sends to channels. Subscribers can process events at their own pace using buffered channels. The `select` statement enables timeout handling, cancellation via `context`, and multiplexing across multiple event sources—none of which come free with callbacks. > **Common Pitfall** > > Forgetting to close subscriber channels causes goroutine leaks. Every `Subscribe` should have a corresponding `Unsubscribe` that closes the channel and removes it from the bus. ## Conclusion - The Functional Options pattern replaces builders and config structs with a clean, extensible API that never breaks existing callers - Strategy in Go maps to interface-based polymorphism—define the interface at the consumer, not the provider - Factory functions enforce invariants that zero-value initialization cannot; return errors for runtime input, panic only for programmer bugs - Channel-based Observer leverages goroutines and select for concurrent, decoupled event delivery without callback chains - HTTP middleware chains compose through the `func(http.Handler) http.Handler` signature, identical across the standard library and third-party routers - Struct embedding provides delegation, not inheritance—understanding this distinction separates Go-literate developers from those translating Java patterns verbatim - Interview success requires demonstrating idiomatic adaptation of patterns to Go's type system, not textbook recitation of Gang of Four definitions --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/go/go-design-patterns-essential-patterns-interview