← Files GophersARCHIVED FILE

skills/go-functional-options/references/option-evolution.md

2.49 KB · Oct 4, 2026 · 12:30 UTC

↓ Download file

# Evolving an Option API

Once `Option` ships, you live with it forever. Plan for change.

## Adding a New Option

Adding a new setting is the easy case: add a field to `options`, write the option type and `With*` constructor. Existing callers are unaffected.

```go
type options struct {
    cache   bool
    logger  *zap.Logger
    metrics MetricsSink // new
}

type metricsOption struct{ m MetricsSink }
func (mo metricsOption) apply(o *options) { o.metrics = mo.m }
func WithMetrics(m MetricsSink) Option   { return metricsOption{m: m} }
```

## Deprecating an Option

Mark the constructor `Deprecated:` in its doc comment and forward its behavior to the replacement:

```go
// Deprecated: use WithLogger; WithLog will be removed in v2.
func WithLog(l Logger) Option { return WithLogger(adaptLogger(l)) }
```

Keep the old `With*` constructor; do not change its signature.

## Option Validation

Option application is a great place to validate, but errors must travel back to the caller. Two common shapes:

### Validate in the constructor

```go
func Open(addr string, opts ...Option) (*Connection, error) {
    o := options{ /* defaults */ }
    for _, opt := range opts {
        opt.apply(&o)
    }
    if o.timeout < 0 {
        return nil, fmt.Errorf("negative timeout %v", o.timeout)
    }
    ...
}
```

### Apply that returns an error

```go
type Option interface {
    apply(*options) error
}
```

Use the error variant when individual options can be invalid in isolation (e.g., parsing a URL). The plain signature is enough in most cases.

## Conditional Options

```go
opts := []db.Option{db.WithLogger(log)}
if cfg.Cache {
    opts = append(opts, db.WithCache(true))
}
conn, err := db.Open(addr, opts...)
```

Callers build the slice; you do not need to design "conditional options" inside the package.

## Variant: Builder

Some APIs surface a builder for chains of options:

```go
b := db.NewBuilder(addr).WithLogger(log).WithCache(true)
conn, err := b.Open()
```

Builders trade a tiny ergonomic win for double the surface area. Prefer plain functional options unless you have a compelling reason.

## Forward Compatibility

A package that exports `type Option interface { apply(*options) }` may safely:

- Add methods to `Option` only if they are unexported (won't break implementers — there are none, by design).
- Add fields to `options`.
- Add new `With*` constructors.

It may **not**:

- Change the signature of an existing `With*`.
- Change defaults silently (announce major-version bumps).
- Export the `apply` method.

SHA-256: 7ee89251e33fb099b62c836663faab0b1d27197c28c1f61264bed2d4e9cdefa9