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.
Resource Routing
references/OPTIONS-VS-STRUCTS.md- Read when choosing between config structs and functional options, implementing the full interface-based option pattern, or evaluating hybrid constructor APIs.
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
optionsstruct - holds all configuration - Exported
Optioninterface - with unexportedapplymethod - 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.
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?
The interface approach is preferred over closure-only options 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 defaults
type options struct {
field1 Type1
field2 Type2
}
// 2. Exported Option interface, unexported method
type Option interface {
apply(*options)
}
// 3. Option type + apply + With* constructor
type field1Option Type1
func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option { return field1Option(v) }
// 4. Constructor applies options over defaults
func 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
Related Skills
- Interface design: See go-interfaces when designing the
Optioninterface 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
Optiontypes,With*functions, or constructor behavior
External Resources
- Self-referential functions and the design of options - Rob Pike
- Functional options for friendly APIs - Dave Cheney