go-functional-options

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Functional Options Pattern

函数选项模式

Source: Uber Go Style Guide
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.
来源:Uber Go Style Guide
函数选项是一种设计模式,你需要声明一个不透明的
Option
类型,该类型会在内部结构体中记录配置信息。构造函数接受可变数量的这类选项,并应用它们来配置最终实例。

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)
在以下场景中使用函数选项模式:
  • 3个及以上可选参数的构造函数或公共API
  • 需要可扩展的API,未来可能新增配置项
  • 重视简洁的调用体验(无需传递默认值)

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
  1. 未导出的
    options
    结构体
    - 存储所有配置信息
  2. 导出的
    Option
    接口
    - 包含未导出的
    apply
    方法
  3. 选项类型 - 实现上述接口
  4. With*
    构造函数
    - 创建配置选项

Option Interface

选项接口

go
type Option interface {
    apply(*options)
}
The unexported
apply
method ensures only options from this package can be used.
go
type Option interface {
    apply(*options)
}
未导出的
apply
方法确保只有当前包内的选项可以被使用。

Complete Implementation

完整实现示例

Source: Uber Go Style Guide
go
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
}
来源:Uber Go Style Guide
go
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

用法示例

Source: Uber Go Style Guide
来源:Uber Go Style Guide

Without Functional Options (Bad)

不使用函数选项模式(不推荐)

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

使用函数选项模式(推荐)

go
// 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),
)
go
// 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

对比:函数选项模式 vs 配置结构体模式

AspectFunctional OptionsConfig Struct
ExtensibilityAdd new
With*
functions
Add 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.
对比维度函数选项模式配置结构体模式
可扩展性新增
With*
函数即可
新增字段(可能导致兼容性问题)
默认值处理内置在构造函数中零值或单独维护默认值
调用方体验仅需指定与默认值不同的配置必须构造完整的结构体
可测试性选项可直接比较结构体比较
复杂度需编写更多模板代码实现更简单
优先选择配置结构体的场景:可选参数少于3个、配置项极少变更、通常需要指定所有配置项,或仅用于内部API时。

Why Not Closures?

为什么不使用闭包?

Source: Uber Go Style Guide
An alternative implementation uses closures:
go
// 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
来源:Uber Go Style Guide
另一种实现方式是使用闭包:
go
// Closure approach (not recommended)
type Option func(*options)

func WithCache(c bool) Option {
    return func(o *options) { o.cache = c }
}
推荐使用接口实现方式的原因:
  1. 可测试性 - 选项可在测试和模拟中直接比较
  2. 可调试性 - 选项可实现
    fmt.Stringer
    接口以支持打印
  3. 灵活性 - 选项可实现额外的接口
  4. 可见性 - 选项类型会在文档中清晰展示

Quick Reference

快速参考模板

go
// 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)
    }
    // ...
}
go
// 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
  • options
    结构体为未导出类型
  • Option
    接口包含未导出的
    apply
    方法
  • 每个配置项都有对应的
    With*
    构造函数
  • 在应用选项前已设置好默认值
  • 必填参数与
    ...Option
    可变参数分离

See Also

相关参考