Go functional options
26 production-grade Go skills for Claude Code, Gemini CLI, and opencode.
npx -y skills add muratmirgun/gophers --skill go-functional-optionsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when designing a Go constructor or factory with 3+ optional parameters, or an API expected to grow new options over time. Covers the canonical Option interface pattern with unexported apply method, With* constructors, default values, and the interface-vs-closure tradeoff. Apply proactively when reviewing a New* function that takes many settings, even if the user didn't ask about functional options. Does not cover general function design (see go-functions).
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
6.3 KB, as published. Nobody here has run it
Functional Options
The functional options pattern lets a constructor stay backward compatible while accepting an open-ended set of optional settings. Callers pass only what differs from the defaults; new options never break old call sites.
Core Rules
- Reach for functional options at 3+ optional parameters or whenever the API will grow.
- The
optionsstruct is unexported. Only the package owns its shape. - The
Optioninterface has an unexportedapplymethod. No external package can forge an option. - Defaults go inside the constructor, before options are applied.
- Required parameters stay positional; only the optional ones go through
...Option. - Prefer the interface form over closures — it composes better with testing, debugging, and
fmt.Stringer.
When to Use What
| Situation | Pattern |
|---|---|
| 0–2 optional params, stable API | Plain positional or named args |
| Config that callers usually pass whole | Config struct |
| 3+ optional params, growing API | Functional options |
| Mix of "must set together" + "rare overrides" | Config struct + small Option set |
Read references/options-vs-struct.md when choosing between options and a plain config struct, or designing a hybrid.
The Canonical Pattern
package db
import "go.uber.org/zap"
// options is the package's private bag of settings.
type options struct {
cache bool
logger *zap.Logger
}
// Option configures Open.
type Option interface {
apply(*options)
}
// --- cacheOption -----------------------------------------------------------
type cacheOption bool
func (c cacheOption) apply(o *options) { o.cache = bool(c) }
// WithCache enables or disables the in-memory cache.
func WithCache(enabled bool) Option { return cacheOption(enabled) }
// --- loggerOption ----------------------------------------------------------
type loggerOption struct{ log *zap.Logger }
func (l loggerOption) apply(o *options) { o.logger = l.log }
// WithLogger sets the logger used by the connection.
func WithLogger(log *zap.Logger) Option { return loggerOption{log: log} }
// --- constructor -----------------------------------------------------------
// Open dials addr using the given options.
func Open(addr string, opts ...Option) (*Connection, error) {
o := options{
cache: true,
logger: zap.NewNop(),
}
for _, opt := range opts {
opt.apply(&o)
}
// ... build the connection from o
return &Connection{}, nil
}
Caller Experience
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false), db.WithLogger(log))
Compare to the alternative where all defaults must be repeated:
db.Open(addr, db.DefaultCache, zap.NewNop()) // tedious
Why an Interface, Not a Closure?
// The closure variant — discouraged
type Option func(*options)
The interface form wins on:
- Testability — option values can be compared in tests.
- Debuggability — option types can implement
fmt.Stringer. - Documentation —
godoclists each option type explicitly. - Extensibility — options can implement additional interfaces (e.g.,
Validate()).
Closures are shorter to write; they pay for that shortness in introspection.
Defaults
Set defaults before applying options. A constructor that ignores its defaults is a bug magnet:
o := options{
cache: true,
logger: zap.NewNop(),
}
for _, opt := range opts {
opt.apply(&o)
}
If a default needs computation (a temp dir, a process-wide ID), build it once during the constructor — not at package init.
Quick Reference
// 1. Unexported settings bag
type options struct { ... }
// 2. Exported interface, unexported method
type Option interface { apply(*options) }
// 3. One option type per setting
type widgetOption Widget
func (w widgetOption) apply(o *options) { o.widget = Widget(w) }
func WithWidget(w Widget) Option { return widgetOption(w) }
// 4. Constructor: defaults, then apply
func New(required string, opts ...Option) (*Thing, error) {
o := options{ /* defaults */ }
for _, opt := range opts { opt.apply(&o) }
return build(required, o)
}
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
Exporting the options struct | External code mutates internals | Keep it unexported |
Option with an exported Apply | Anyone can build an option | Unexported apply method |
| Applying options before defaults | Defaults overwrite caller intent | Defaults first, then apply |
func Option(*options) closures | Opaque in tests/logs | Interface form |
| 7+ positional required params | Caller error-prone | Promote them into a config or options |
Mixing required and optional through ...Option | Required is no longer required | Keep required positional |
Verification Checklist
-
optionsis unexported -
Optioninterface has an unexportedapply(*options)method - Each setting has a
With*constructor returningOption - Constructor sets defaults first, then applies options
- Required parameters are not hidden behind
...Option - No exported
ApplyorOption func(*options)slipped in - Doc comments explain each
With*and its default
References
- references/options-vs-struct.md — when to prefer a config struct, and how to combine the two