Skillshub go-functional-options
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).
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cxuu/golang-skills/go-functional-options" ~/.claude/skills/comeonoliver-skillshub-go-functional-options && rm -rf "$T"
skills/cxuu/golang-skills/go-functional-options/SKILL.mdFunctional 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
struct - holds all configurationoptions - Exported
interface - with unexportedOption
methodapply - Option types - implement the interface
constructors - create optionsWith*
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" // options holds all configuration for opening a connection. type options struct { cache bool logger *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 bool func (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 defaults options := options{ cache: defaultCache, logger: zap.NewNop(), } // Apply all provided options for _, 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 defaults db.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 needed 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 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 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
-
struct is unexportedoptions -
interface has unexportedOption
methodapply - Each option has a
constructorWith* - Defaults are set before applying options
- Required parameters are separate from
...Option
Related Skills
- Interface design: See go-interfaces when designing the
interface or choosing between interface and closure approachesOption - Naming conventions: See go-naming when naming
constructors, option types, or the unexported options structWith* - Function design: See go-functions when organizing constructors within a file or formatting variadic signatures
- Documentation: See go-documentation when documenting
types,Option
functions, or constructor behaviorWith*
External Resources
- Self-referential functions and the design of options - Rob Pike
- Functional options for friendly APIs - Dave Cheney