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).

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "go-functional-options" with this command: npx skills add cxuu/golang-skills/cxuu-golang-skills-go-functional-options

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.

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.

Complete Implementation

package db

import "go.uber.org/zap"

// options holds all configuration for opening a connection.
type options struct {
    cache  bool
    logger *zap.Logger
}

// Option configures how we open the connection.
type Option interface {
    apply(*options)
}

// cacheOption implements Option for cache setting (simple type alias).
type cacheOption bool

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

// WithCache enables or disables caching.
func WithCache(c bool) Option {
    return cacheOption(c)
}

// loggerOption implements Option for logger setting (struct for pointers).
type loggerOption struct {
    Log *zap.Logger
}

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

// WithLogger sets the logger for the connection.
func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

// Open creates a connection.
func Open(addr string, opts ...Option) (*Connection, error) {
    // Start with defaults
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

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

    // Use options.cache and options.logger...
    return &Connection{}, nil
}

Usage Examples

Without Functional Options (Bad)

// Caller must always provide all parameters, even defaults
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)

With Functional Options (Good)

// Only provide options when needed
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(log),
)

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.

Read references/OPTIONS-VS-STRUCTS.md when deciding between functional options and config structs, designing a config struct API with proper defaults, or evaluating the hybrid approach for complex constructors.

Why Not Closures?

An alternative implementation uses closures:

// Closure approach (not recommended)
type Option func(*options)

func WithCache(c bool) Option {
    return func(o *options) { o.cache = c }
}

The interface approach is preferred 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

go-linting

No summary provided by upstream source.

Repository SourceNeeds Review
232-cxuu
General

go-testing

No summary provided by upstream source.

Repository SourceNeeds Review
220-cxuu
General

go-documentation

No summary provided by upstream source.

Repository SourceNeeds Review
214-cxuu
General

go-naming

No summary provided by upstream source.

Repository SourceNeeds Review
209-cxuu