| name | go-functional-options |
| description | Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions). |
| license | Apache-2.0 |
| metadata | {"sources":"Uber Style Guide"} |
Functional Options Pattern
Functional options is a pattern where you declare an opaque Option type that records information in an internal struct. The constructor accepts a variadic number of these options and applies them to configure the result.
When to Use
Use functional options when:
- 3+ optional arguments on constructors or public APIs
- Extensible APIs that may gain new options over time
- Clean caller experience is important (no need to pass defaults)
The Pattern
Core Components
- Unexported
options struct - holds all configuration
- Exported
Option interface - with unexported apply method
- Option types - implement the interface
With* constructors - create options
Option Interface
type Option interface {
apply(*options)
}
The unexported apply method ensures only options from this package can be used.
Complete Implementation
package db
import "go.uber.org/zap"
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
func Open(addr string, opts ...Option) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
return &Connection{}, nil
}
Usage Examples
Without Functional Options (Bad)
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false , zap.NewNop())
db.Open(addr, false , log)
With Functional Options (Good)
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
)
Comparison: Functional Options vs Config Struct
| Aspect | Functional Options | Config Struct |
|---|
| Extensibility | Add new With* functions | Add new fields (may break) |
| Defaults | Built into constructor | Zero values or separate defaults |
| Caller experience | Only specify what differs | Must construct entire struct |
| Testability | Options are comparable | Struct comparison |
| Complexity | More boilerplate | Simpler setup |
Prefer Config Struct when: Fewer than 3 options, options rarely change, all options usually specified together, or internal APIs only.
Read references/OPTIONS-VS-STRUCTS.md when deciding between functional options and config structs, designing a config struct API with proper defaults, or evaluating the hybrid approach for complex constructors.
Why Not Closures?
An alternative implementation uses closures:
type Option func(*options)
func WithCache(c bool) Option {
return func(o *options) { o.cache = c }
}
The interface approach is preferred because:
- Testability - Options can be compared in tests and mocks
- Debuggability - Options can implement
fmt.Stringer
- Flexibility - Options can implement additional interfaces
- Visibility - Option types are visible in documentation
Quick Reference
type options struct {
field1 Type1
field2 Type2
}
type Option interface {
apply(*options)
}
type field1Option Type1
func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option { return field1Option(v) }
func New(required string, opts ...Option) (*Thing, error) {
o := options{field1: defaultField1, field2: defaultField2}
for _, opt := range opts {
opt.apply(&o)
}
}
Checklist
Related Skills
- Interface design: See go-interfaces when designing the
Option interface or choosing between interface and closure approaches
- Naming conventions: See go-naming when naming
With* constructors, option types, or the unexported options struct
- Function design: See go-functions when organizing constructors within a file or formatting variadic signatures
- Documentation: See go-documentation when documenting
Option types, With* functions, or constructor behavior
External Resources