Awesome-omni-skill go-functional-options
The functional options pattern for Go constructors and public APIs. Use when designing APIs with optional configuration, especially with 3+ parameters.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/go-functional-options-majiayu000" ~/.claude/skills/diegosouzapw-awesome-omni-skill-go-functional-options-dabed1 && rm -rf "$T"
manifest:
skills/development/go-functional-options-majiayu000/SKILL.mdsource content
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
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
Source: Uber Go Style Guide
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
Source: Uber Go Style Guide
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.
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 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
See Also
- Core Go style principlesgo-style-core
- Naming conventions for Gogo-naming
- Defensive programming patternsgo-defensive- Self-referential functions and the design of options - Rob Pike
- Functional options for friendly APIs - Dave Cheney