Skill v1.0.1
currentAutomated scan100/1002 files
version: "1.0.1" 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
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
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
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.
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:
// 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
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