Introduction
Constructors with many parameters are hard to read, maintain, and extend. The functional options pattern solves this by using variadic closure arguments that each configure one aspect of the object. This is one of Go's most celebrated API design patterns, used in production libraries like gRPC, Zap, and the AWS SDK.
Key Concepts
- Option Type: A function type (e.g.,
type Option func(*Server)) that modifies the object being constructed. - With Functions*: Named constructors like
WithTimeout()that return an Option, making each configuration self-documenting. - Variadic Constructor: A
New*function that accepts...Option, allowing zero or more configuration options.
Real World Context
You are building an HTTP client library. Users need to configure timeout, retry count, base URL, auth headers, and TLS settings. With a traditional constructor, NewClient(url, timeout, retries, headers, tls) becomes unreadable. Functional options let users write NewClient(WithBaseURL(url), WithTimeout(5*time.Second)) — clear, flexible, and extensible.
Deep Dive
The pattern has three parts. First, define the Option type:
gotype Server struct { Timeout time.Duration Logger Logger } type Option func(*Server)
Second, create With* functions for each configurable field:
gofunc WithTimeout(d time.Duration) Option { return func(s *Server) { s.Timeout = d } } func WithLogger(l Logger) Option { return func(s *Server) { s.Logger = l } }
Third, the constructor applies defaults then options:
gofunc NewServer(opts ...Option) *Server { s := &Server{ Timeout: 30 * time.Second, // Default Logger: defaultLogger, } for _, opt := range opts { opt(s) } return s }
Callers compose exactly what they need:
gosvc := NewServer( WithTimeout(5 * time.Second), WithLogger(myLogger), )
Common Pitfalls
- Not setting defaults before applying options — Always initialize the struct with sensible defaults first. Options override them.
- Creating options that conflict — Document when two options are mutually exclusive (e.g., WithTLS and WithInsecure).
Best Practices
- Name options with the With prefix —
WithTimeout,WithLogger,WithRetry. This is the universal Go convention. - Keep the default constructor usable —
NewServer()with zero options should produce a working server with sensible defaults.
Summary
- Functional options replace long parameter lists with named, composable configuration.
- Define
type Option func(*T)andWith*functions for each setting. - Always set defaults before applying options.
- The pattern enables backwards-compatible API evolution—new options never break existing callers.
Code Examples
// WithLogger returns an Option that sets the server's logger.
// Each With* function configures one aspect of the server.
func WithLogger(l *Logger) Option {
return func(s *Server) {
s.logger = l
}
}
// Usage: NewServer(WithLogger(myLogger), WithTimeout(5*time.Second))