concept

Functional Options Pattern

created 2026-05-05 go · patterns · idioms · constructors · api-design

Functional Options Pattern

A Go idiom for constructors with many optional parameters. Instead of overloaded constructors (Java) or kwargs (Python), you define an Option function type that mutates the constructed value, and pass a variadic list of those at construction.

Shape

type client struct {
    baseURL string
    timeout time.Duration
}

type Option func(*client)

func WithBaseURL(baseURL string) Option {
    return func(c *client) { c.baseURL = baseURL }
}

func WithTimeout(t time.Duration) Option {
    return func(c *client) { c.timeout = t }
}

func NewClient(opts ...Option) *client {
    c := &client{
        baseURL: "https://default",
        timeout: 5 * time.Second,
    }
    for _, opt := range opts {
        opt(c)
    }
    return c
}

// Use:
c := NewClient(WithBaseURL("https://api.example.com"), WithTimeout(10*time.Second))

Why this beats the alternatives

AlternativeProblem
NewClient(baseURL, timeout, retries, ...) positional argsBreaks every caller when you add a parameter
NewClient(Config{...}) struct literalHard to validate per-field; zero-values are ambiguous (did the caller mean 0 or “default”?)
Builder patternVerbose, not idiomatic Go

Functional options solve all three: order-independent, additive, defaults are explicit in the constructor body, and each option can validate or compose.

When to use

  • Public packages with > 3 optional parameters or any parameter that’s likely to grow over time. Examples: grpc.NewServer, http.NewServeMux accessor patterns, every modern Go SDK (AWS, GCP, etc.).
  • Shared pkg/ HTTP clients in a go-layered-architecture — exactly the case the source article uses it for.

When not to use

  • Constructors with 0-1 parameters — just take the parameter directly.
  • Internal types where you control all callers — a config struct is fine and simpler.
  • Hot paths — each Option is a closure allocation. Negligible normally; matters in tight loops.

Variants

  • Errored options: type Option func(*client) error — lets the option itself fail (e.g., parsing a URL). Constructor returns (client, error).
  • Method options: same shape but applied to existing instances for runtime reconfiguration. Less common; usually a code smell.
  • Embedded options: option packages can embed other option packages (e.g., WithRetry from a retry sub-package), enabling cross-cutting middleware-shaped options.
  • go-layered-architecture — where this pattern most naturally lives (shared client constructors in pkg/)
  • gin, gorm — both use this pattern in their public APIs