Skip to content
Back to skills

Go Functional Options

ASecurity

Use when designing a Go constructor or factory with 3+ optional parameters,

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
ai-agentsgobashtestingdebugginggitapidocumentation

Works with

  • claude code
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add muratmirgun/gophers --skill go-functional-options --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Go Functional Options?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Go Functional Options
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/muratmirgun-go-functional-options-gophers/badge)](https://www.skillsdirectory.com/skills/muratmirgun-go-functional-options-gophers)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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).
user-invocable: false
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:*)
metadata:
  openclaw:
    emoji: ⚙️
    homepage: https://github.com/muratmirgun/gophers
    requires:
      bins:
      - go
    install: []
---

# 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

| 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](../../../skills/go-functional-options/references/options-vs-struct.md) when choosing between options and a plain config struct, or designing a hybrid.

## The Canonical Pattern

```go
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

```go
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:

```go
db.Open(addr, db.DefaultCache, zap.NewNop()) // tedious
```

## Why an Interface, Not a Closure?

```go
// 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.

## Defaults

Set defaults *before* applying options. A constructor that ignores its defaults is a bug magnet:

```go
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

```go
// 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

- [ ] `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

- [references/options-vs-struct.md](../../../skills/go-functional-options/references/options-vs-struct.md) — when to prefer a config struct, and how to combine the two

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.