← GophersCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Gophers
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "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).",
"included_files": [
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 233
},
{
"relative_path": "references/option-evolution.md",
"size_in_bytes": 2549
},
{
"relative_path": "references/options-vs-struct.md",
"size_in_bytes": 2439
}
],
"name": "go-functional-options",
"skill_md_contents": "---\nname: go-functional-options\ndescription: \"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).\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Plain Go (any supported version).\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Functional Options\n\nThe 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.\n\n## Core Rules\n\n1. **Reach for functional options at 3+ optional parameters** or whenever the API will grow.\n2. **The `options` struct is unexported.** Only the package owns its shape.\n3. **The `Option` interface has an unexported `apply` method.** No external package can forge an option.\n4. **Defaults go inside the constructor**, before options are applied.\n5. **Required parameters stay positional;** only the optional ones go through `...Option`.\n6. **Prefer the interface form over closures** — it composes better with testing, debugging, and `fmt.Stringer`.\n\n## When to Use What\n\n| Situation | Pattern |\n|---|---|\n| 0–2 optional params, stable API | Plain positional or named args |\n| Config that callers usually pass whole | Config struct |\n| 3+ optional params, growing API | **Functional options** |\n| Mix of \"must set together\" + \"rare overrides\" | Config struct + small `Option` set |\n\n> Read [references/options-vs-struct.md](references/options-vs-struct.md) when choosing between options and a plain config struct, or designing a hybrid.\n\n## The Canonical Pattern\n\n```go\npackage db\n\nimport \"go.uber.org/zap\"\n\n// options is the package's private bag of settings.\ntype options struct {\n cache bool\n logger *zap.Logger\n}\n\n// Option configures Open.\ntype Option interface {\n apply(*options)\n}\n\n// --- cacheOption -----------------------------------------------------------\n\ntype cacheOption bool\n\nfunc (c cacheOption) apply(o *options) { o.cache = bool(c) }\n\n// WithCache enables or disables the in-memory cache.\nfunc WithCache(enabled bool) Option { return cacheOption(enabled) }\n\n// --- loggerOption ----------------------------------------------------------\n\ntype loggerOption struct{ log *zap.Logger }\n\nfunc (l loggerOption) apply(o *options) { o.logger = l.log }\n\n// WithLogger sets the logger used by the connection.\nfunc WithLogger(log *zap.Logger) Option { return loggerOption{log: log} }\n\n// --- constructor -----------------------------------------------------------\n\n// Open dials addr using the given options.\nfunc Open(addr string, opts ...Option) (*Connection, error) {\n o := options{\n cache: true,\n logger: zap.NewNop(),\n }\n for _, opt := range opts {\n opt.apply(&o)\n }\n // ... build the connection from o\n return &Connection{}, nil\n}\n```\n\n### Caller Experience\n\n```go\ndb.Open(addr)\ndb.Open(addr, db.WithLogger(log))\ndb.Open(addr, db.WithCache(false), db.WithLogger(log))\n```\n\nCompare to the alternative where all defaults must be repeated:\n\n```go\ndb.Open(addr, db.DefaultCache, zap.NewNop()) // tedious\n```\n\n## Why an Interface, Not a Closure?\n\n```go\n// The closure variant — discouraged\ntype Option func(*options)\n```\n\nThe interface form wins on:\n\n1. **Testability** — option values can be compared in tests.\n2. **Debuggability** — option types can implement `fmt.Stringer`.\n3. **Documentation** — `godoc` lists each option type explicitly.\n4. **Extensibility** — options can implement additional interfaces (e.g., `Validate()`).\n\nClosures are shorter to write; they pay for that shortness in introspection.\n\n## Defaults\n\nSet defaults *before* applying options. A constructor that ignores its defaults is a bug magnet:\n\n```go\no := options{\n cache: true,\n logger: zap.NewNop(),\n}\nfor _, opt := range opts {\n opt.apply(&o)\n}\n```\n\nIf a default needs computation (a temp dir, a process-wide ID), build it once during the constructor — not at package init.\n\n## Quick Reference\n\n```go\n// 1. Unexported settings bag\ntype options struct { ... }\n\n// 2. Exported interface, unexported method\ntype Option interface { apply(*options) }\n\n// 3. One option type per setting\ntype widgetOption Widget\nfunc (w widgetOption) apply(o *options) { o.widget = Widget(w) }\nfunc WithWidget(w Widget) Option { return widgetOption(w) }\n\n// 4. Constructor: defaults, then apply\nfunc New(required string, opts ...Option) (*Thing, error) {\n o := options{ /* defaults */ }\n for _, opt := range opts { opt.apply(&o) }\n return build(required, o)\n}\n```\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| Exporting the `options` struct | External code mutates internals | Keep it unexported |\n| `Option` with an **exported** `Apply` | Anyone can build an option | Unexported `apply` method |\n| Applying options before defaults | Defaults overwrite caller intent | Defaults first, then `apply` |\n| `func Option(*options)` closures | Opaque in tests/logs | Interface form |\n| 7+ positional required params | Caller error-prone | Promote them into a config or options |\n| Mixing required and optional through `...Option` | Required is no longer required | Keep required positional |\n\n## Verification Checklist\n\n- [ ] `options` is unexported\n- [ ] `Option` interface has an unexported `apply(*options)` method\n- [ ] Each setting has a `With*` constructor returning `Option`\n- [ ] Constructor sets defaults first, then applies options\n- [ ] Required parameters are not hidden behind `...Option`\n- [ ] No exported `Apply` or `Option func(*options)` slipped in\n- [ ] Doc comments explain each `With*` and its default\n\n## References\n\n- [references/options-vs-struct.md](references/options-vs-struct.md) — when to prefer a config struct, and how to combine the two\n"
}SHA-256 of public snapshot: 830f0d0e66aaea67acaaddbcab5e9a79c2e7ec45364b43042b229955b0685900