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
| Alternative | Problem |
|---|---|
NewClient(baseURL, timeout, retries, ...) positional args | Breaks every caller when you add a parameter |
NewClient(Config{...}) struct literal | Hard to validate per-field; zero-values are ambiguous (did the caller mean 0 or “default”?) |
| Builder pattern | Verbose, 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.NewServeMuxaccessor 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
Optionis 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.,
WithRetryfrom a retry sub-package), enabling cross-cutting middleware-shaped options.
Related
- go-layered-architecture — where this pattern most naturally lives (shared client constructors in
pkg/) - gin, gorm — both use this pattern in their public APIs