agentsclimarketplace

Go functional options

Skill muratmirgun/gophers/skills/go-functional-options

26 production-grade Go skills for Claude Code, Gemini CLI, and opencode.

Install
npx -y skills add muratmirgun/gophers --skill go-functional-options

Assembled 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

  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.

When to Use What

SituationPattern
0–2 optional params, stable APIPlain positional or named args
Config that callers usually pass wholeConfig struct
3+ optional params, growing APIFunctional 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:

  1. Testability — option values can be compared in tests.
  2. Debuggability — option types can implement fmt.Stringer.
  3. Documentationgodoc 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.

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-patternWhy it hurtsDo this instead
Exporting the options structExternal code mutates internalsKeep it unexported
Option with an exported ApplyAnyone can build an optionUnexported apply method
Applying options before defaultsDefaults overwrite caller intentDefaults first, then apply
func Option(*options) closuresOpaque in tests/logsInterface form
7+ positional required paramsCaller error-pronePromote them into a config or options
Mixing required and optional through ...OptionRequired is no longer requiredKeep required positional

Verification Checklist

  • options is unexported
  • Option interface has an unexported apply(*options) method
  • Each setting has a With* constructor returning Option
  • Constructor sets defaults first, then applies options
  • Required parameters are not hidden behind ...Option
  • No exported Apply or Option func(*options) slipped in
  • Doc comments explain each With* and its default

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.