go-clean-architecture
Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase,…
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.
Claude Code
$ npx -y skills add muratmirgun/gophers --skill go-functional-options --agent claude-code
Codex
$ npx skills add muratmirgun/gophers --skill go-functional-options -a codex -y
Cursor
$ npx skills add muratmirgun/gophers --skill go-functional-options -a cursor -y
Gemini CLI
$ npx skills add muratmirgun/gophers --skill go-functional-options -a gemini-cli -y
GitHub Copilot
$ npx skills add muratmirgun/gophers --skill go-functional-options -a github-copilot -y
OpenCode
$ npx skills add muratmirgun/gophers --skill go-functional-options -a opencode -y
Windsurf
$ npx skills add muratmirgun/gophers --skill go-functional-options -a windsurf -y
name: go-functional-options 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)." license: MIT compatibility: "Designed for Claude Code or similar AI coding agents. Plain Go (any supported version)." allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)
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.
1. **Reach for functional options at 3+ optional parameters** or whenever the API will grow. 2. **The `options` struct is unexported.** Only the package owns its shape. 3. **The `Option` interface has an unexported `apply` method.** No external package can forge an option. 4. **Defaults go inside the constructor**, before options are applied. 5. **Required parameters stay positional;** only the optional ones go through `...Option`. 6. **Prefer the interface form over closures** — it composes better with testing, debugging, and `fmt.Stringer`.
| 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](references/options-vs-struct.md) when choosing between options and a plain config struct, or designing a hybrid.
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
}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
// The closure variant — discouraged type Option func(*options)
The interface form wins on:
1. **Testability** — option values can be compared in tests. 2. **Debuggability** — option types can implement `fmt.Stringer`. 3. **Documentation** — `godoc` lists each option type explicitly. 4. **Extensibility** — options can implement additional interfaces (e.g., `Validate()`).
Closures are shorter to write; they pay for that shortness in introspection.
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.
// 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-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 |
26 production-grade Go skills for Claude Code, Gemini CLI, and opencode. Battle-tested patterns from the Go community — codified as triggerable AI skills.
Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase,…
Invoke this skill to systematically review a Go change against community style standards before merging. Walks the diff topic by…
Use when writing or reviewing Go code for clarity, formatting, control flow, variable declarations, switch usage, and function…
Use when writing or reviewing concurrent Go code — goroutines, channels, select, mutexes, atomics, errgroup, singleflight, worker…
Use when designing, propagating, or debugging context.Context flow in Go — first-parameter placement, deadlines and cancellation,…
Use when writing conditionals, loops, switches, type switches, or blank-identifier patterns in Go. Covers if-with-initialization,…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.