返回 Skills
cxuu/golang-skills· Apache-2.0 内容可用

go-functional-options

Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: go-functional-options description: Use when designing a Go constructor or factory function with optional configuration — especially with 3+ optional parameters or extensible APIs. Also use when building a New* function that takes many settings, even if they don't mention "functional options" by name. Does not cover general function design (see go-functions).

Functional Options Pattern

Functional options is a pattern where you declare an opaque Option type that records information in an internal struct. The constructor accepts a variadic number of these options and applies them to configure the result.

Resource Routing

  • references/OPTIONS-VS-STRUCTS.md - Read when choosing between config structs and functional options, implementing the full interface-based option pattern, or evaluating hybrid constructor APIs.

When to Use

Use functional options when:

  • 3+ optional arguments on constructors or public APIs
  • Extensible APIs that may gain new options over time
  • Clean caller experience is important (no need to pass defaults)

The Pattern

Core Components

  1. Unexported options struct - holds all configuration
  2. Exported Option interface - with unexported apply method
  3. Option types - implement the interface
  4. With* constructors - create options

Option Interface

type Option interface {
    apply(*options)
}

The unexported apply method ensures only options from this package can be used.

Comparison: Functional Options vs Config Struct

AspectFunctional OptionsConfig Struct
ExtensibilityAdd new With* functionsAdd new fields (may break)
DefaultsBuilt into constructorZero values or separate defaults
Caller experienceOnly specify what differsMust construct entire struct
TestabilityOptions are comparableStruct comparison
ComplexityMore boilerplateSimpler setup

Prefer Config Struct when: Fewer than 3 options, options rarely change, all options usually specified together, or internal APIs only.

Why Not Closures?

The interface approach is preferred over closure-only options because:

  1. Testability - Options can be compared in tests and mocks
  2. Debuggability - Options can implement fmt.Stringer
  3. Flexibility - Options can implement additional interfaces
  4. Visibility - Option types are visible in documentation

Quick Reference

// 1. Unexported options struct with defaults
type options struct {
    field1 Type1
    field2 Type2
}

// 2. Exported Option interface, unexported method
type Option interface {
    apply(*options)
}

// 3. Option type + apply + With* constructor
type field1Option Type1

func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option            { return field1Option(v) }

// 4. Constructor applies options over defaults
func New(required string, opts ...Option) (*Thing, error) {
    o := options{field1: defaultField1, field2: defaultField2}
    for _, opt := range opts {
        opt.apply(&o)
    }
    // ...
}

Checklist

  • options struct is unexported
  • Option interface has unexported apply method
  • Each option has a With* constructor
  • Defaults are set before applying options
  • Required parameters are separate from ...Option

Related Skills

  • Interface design: See go-interfaces when designing the Option interface or choosing between interface and closure approaches
  • Naming conventions: See go-naming when naming With* constructors, option types, or the unexported options struct
  • Function design: See go-functions when organizing constructors within a file or formatting variadic signatures
  • Documentation: See go-documentation when documenting Option types, With* functions, or constructor behavior

External Resources

附带文件

references/OPTIONS-VS-STRUCTS.md
# Functional Options vs Config Structs

> Sources: source/google-go-styleguide/best-practices.md; source/uber-go-style/style.md
> Authority: project policy
> Minimum Go: any supported Go version
> Last verified: 2026-06-19

Both functional options and config structs solve the same problem — optional
configuration for constructors — but they have different trade-offs. Choose
based on API audience, extensibility needs, and complexity budget.

Project policy: use Google guidance to decide whether optional configuration is
worth the complexity; when functional options are chosen, prefer Uber's
interface-with-unexported-method implementation over closure-only options for
debuggability and testability.

## Contents

- [Decision Framework](#decision-framework)
- [Config Struct Pattern](#config-struct-pattern)
- [Comparison](#comparison)
- [When to Prefer Config Structs](#when-to-prefer-config-structs)
- [When to Prefer Functional Options](#when-to-prefer-functional-options)
- [Caller Ergonomics](#caller-ergonomics)
- [Functional Options Implementation](#functional-options-implementation)
- [Hybrid Approach](#hybrid-approach)

## Decision Framework

```
Need optional configuration?
├─ Internal or test-only API?
│   └─ Config struct (simpler, less boilerplate)
├─ Public API with 3+ options?
│   └─ Functional options (extensible, backward-compatible)
├─ Options need validation or interdependencies?
│   └─ Functional options (validate in apply or constructor)
├─ All options usually specified together?
│   └─ Config struct (one literal, no With* ceremony)
└─ Options likely to grow over time?
    └─ Functional options (add With* without breaking callers)
```

## Config Struct Pattern

A config struct groups optional parameters into a single struct passed to the
constructor. Zero values serve as defaults, or provide a `DefaultConfig()`.

**Good**
```go
type Config struct {
    Timeout  time.Duration // zero = no timeout
    MaxRetry int           // zero = no retries
    Logger   *log.Logger   // nil = discard
}

func NewClient(addr string, cfg Config) *Client {
    if cfg.Logger == nil {
        cfg.Logger = log.New(io.Discard, "", 0)
    }
    return &Client{addr: addr, cfg: cfg}
}
```

```go
c := NewClient("localhost:8080", Config{
    Timeout:  5 * time.Second,
    MaxRetry: 3,
})
```

**Bad** — Relying on unexported config fields in a public API:
```go
type config struct {  // unexported: callers can't construct it
    timeout time.Duration
}

func NewClient(addr string, cfg config) *Client { ... }
```

### When Zero Values Don't Work

If zero is a valid non-default value (e.g., timeout of 0 means "no timeout"
but the desired default is 30s), use a pointer field or a sentinel value:

```go
type Config struct {
    Timeout *time.Duration // nil = use default (30s), zero = no timeout
}
```

## Comparison

| Aspect | Functional Options | Config Struct |
|--------|-------------------|---------------|
| **Boilerplate** | High (type + apply + With* per option) | Low (one struct) |
| **Extensibility** | Add `With*` — no breaking changes | Add field — no breaking changes |
| **Backward compat** | Excellent for public APIs | Good (new fields get zero values) |
| **Defaults** | Built into constructor | Zero values or `DefaultConfig()` |
| **Validation** | In `apply` or constructor loop | In constructor after struct received |
| **Discoverability** | `With*` functions appear in godoc | All fields visible in one struct |
| **Testability** | Compare options or test constructor output | Compare struct literals |
| **Caller experience** | Only specify what differs from defaults | Must construct struct literal |
| **Zero-value ambiguity** | None — unset options not applied | May need pointer fields |

## When to Prefer Config Structs

- **Internal APIs** — less ceremony, easier to read at call sites
- **Few options (1-3)** — functional options overhead not justified
- **All options typically set together** — no benefit to variadic style
- **No validation needed** — simple field assignment suffices
- **Options are data, not behavior** — struct fields map naturally

```go
srv := NewServer(Config{
    Port:    8080,
    TLSCert: "/path/to/cert.pem",
    TLSKey:  "/path/to/key.pem",
})
```

## When to Prefer Functional Options

- **Public/library APIs** — callers shouldn't track internal config evolution
- **3+ options** that are individually optional
- **Complex defaults** — default computation depends on other options
- **Validation per option** — reject bad values at apply time
- **Options may grow** — new `With*` functions are purely additive

```go
srv := NewServer(
    WithPort(8080),
    WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
    WithLogger(logger),
)
```

## Caller Ergonomics

Functional options are most valuable when they keep call sites focused on the
settings that differ from defaults.

**Before** — positional optional arguments hide meaning and force callers to
remember default sentinel values:

```go
conn, err := db.Open(addr, false, zap.NewNop(), 30*time.Second)
```

**After** — options name each non-default setting and allow the rest to stay
inside the constructor:

```go
conn, err := db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(logger),
)
```

## Functional Options Implementation

Use an exported `Option` interface with an unexported `apply` method when
options should be comparable, mockable, or able to implement extra interfaces:

```go
package db

import "go.uber.org/zap"

type options struct {
    cache  bool
    logger *zap.Logger
}

type Option interface {
    apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
    opts.cache = bool(c)
}

func WithCache(c bool) Option {
    return cacheOption(c)
}

type loggerOption struct {
    Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
    opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

func Open(addr string, opts ...Option) (*Connection, error) {
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

    for _, o := range opts {
        o.apply(&options)
    }

    return &Connection{}, nil
}
```

Avoid closure-only options by default:

```go
type Option func(*options)
```

Closure options are concise, but they are harder to compare in tests, harder to
describe in logs, and cannot implement additional interfaces.

## Hybrid Approach

For APIs that need both convenience and extensibility, accept a config struct
for common settings and functional options for advanced overrides:

```go
func NewServer(cfg Config, opts ...Option) *Server {
    s := &Server{cfg: cfg}
    for _, o := range opts {
        o.apply(&s.cfg)
    }
    return s
}
```

Use this sparingly — it adds complexity. Prefer one approach per API.