Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: go-functional-options description: The functional options pattern for Go constructors and public APIs. Use when designing APIs with optional configuration, especially with 3+ parameters.
Functional Options Pattern
Source: Uber Go Style Guide
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
applymethod - 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
Source: Uber Go Style Guide
package dbimport "go.uber.org/zap"// options holds all configuration for opening a connection.type options struct {cache boollogger *zap.Logger}// Option configures how we open the connection.type Option interface {apply(*options)}// cacheOption implements Option for cache setting (simple type alias).type cacheOption boolfunc (c cacheOption) apply(opts *options) {opts.cache = bool(c)}// WithCache enables or disables caching.func WithCache(c bool) Option {return cacheOption(c)}// loggerOption implements Option for logger setting (struct for pointers).type loggerOption struct {Log *zap.Logger}func (l loggerOption) apply(opts *options) {opts.logger = l.Log}// WithLogger sets the logger for the connection.func WithLogger(log *zap.Logger) Option {return loggerOption{Log: log}}// Open creates a connection.func Open(addr string, opts ...Option) (*Connection, error) {// Start with defaultsoptions := options{cache: defaultCache,logger: zap.NewNop(),}// Apply all provided optionsfor _, o := range opts {o.apply(&options)}// Use options.cache and options.logger...return &Connection{}, nil}
Usage Examples
Source: Uber Go Style Guide
Without Functional Options (Bad)
// Caller must always provide all parameters, even defaultsdb.Open(addr, db.DefaultCache, zap.NewNop())db.Open(addr, db.DefaultCache, log)db.Open(addr, false /* cache */, zap.NewNop())db.Open(addr, false /* cache */, log)
With Functional Options (Good)
// Only provide options when neededdb.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.
Why Not Closures?
Source: Uber Go Style Guide
An alternative implementation uses closures:
// Closure approach (not recommended)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
// 1. Unexported options struct with defaultstype options struct {field1 Type1field2 Type2}// 2. Exported Option interface, unexported methodtype Option interface {apply(*options)}// 3. Option type + apply + With* constructortype field1Option Type1func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }func WithField1(v Type1) Option { return field1Option(v) }// 4. Constructor applies options over defaultsfunc New(required string, opts ...Option) (*Thing, error) {o := options{field1: defaultField1, field2: defaultField2}for _, opt := range opts {opt.apply(&o)}// ...}
Checklist
- [ ]
optionsstruct is unexported - [ ]
Optioninterface has unexportedapplymethod - [ ] Each option has a
With*constructor - [ ] Defaults are set before applying options
- [ ] Required parameters are separate from
...Option
See Also
go-style-core- Core Go style principlesgo-naming- Naming conventions for Gogo-defensive- Defensive programming patterns- Self-referential functions and the design of options - Rob Pike
- Functional options for friendly APIs - Dave Cheney