| 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). |
| license | Apache-2.0 |
| metadata | {"sources":"Uber Style Guide"} |
函数式选项模式
函数式选项是一种模式,你声明一个不透明的 Option 类型,在内部结构体中记录信息。构造函数接受可变数量的这些选项并将其应用于配置结果。
何时使用
在以下情况使用函数式选项:
- 构造函数或公共 API 上有 3 个以上可选参数
- 可扩展 API,可能随时间增加新选项
- 良好的调用者体验很重要(无需传递默认值)
模式
核心组件
- 未导出的
options 结构体 - 保存所有配置
- 导出的
Option 接口 - 带有未导出的 apply 方法
- Option 类型 - 实现接口
With* 构造函数 - 创建选项
Option 接口
type Option interface {
apply(*options)
}
未导出的 apply 方法确保只能使用来自本包的选项。
完整实现
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
}
使用示例
不使用函数式选项(不好)
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false , zap.NewNop())
db.Open(addr, false , log)
使用函数式选项(好)
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
)
比较:函数式选项 vs 配置结构体
| 方面 | 函数式选项 | 配置结构体 |
|---|
| 可扩展性 | 添加新的 With* 函数 | 添加新字段(可能破坏兼容性) |
| 默认值 | 内置于构造函数 | 零值或单独的默认值 |
| 调用者体验 | 只指定不同的部分 | 必须构造整个结构体 |
| 可测试性 | 选项可比较 | 结构体比较 |
| 复杂性 | 更多样板代码 | 更简单的设置 |
优先使用配置结构体的场景:少于 3 个选项、选项很少变化、所有选项通常一起指定、或仅用于内部 API。
在决定使用函数式选项还是配置结构体、设计具有适当默认值的配置结构体 API、或评估复杂构造函数的混合方法时,请阅读 references/OPTIONS-VS-STRUCTS.md。
为什么不使用闭包?
另一种实现使用闭包:
type Option func(*options)
func WithCache(c bool) Option {
return func(o *options) { o.cache = c }
}
优先使用接口方法,因为:
- 可测试性 - 选项可以在测试和 mock 中进行比较
- 可调试性 - 选项可以实现
fmt.Stringer
- 灵活性 - 选项可以实现额外的接口
- 可见性 - 选项类型在文档中可见
快速参考
type options struct {
field1 Type1
field2 Type2
}
type Option interface {
apply(*options)
}
type field1Option Type1
func (o field1Option) apply(opts *options) { opts.field1 = Type1(o) }
func WithField1(v Type1) Option { return field1Option(v) }
func New(required string, opts ...Option) (*Thing, error) {
o := options{field1: defaultField1, field2: defaultField2}
for _, opt := range opts {
opt.apply(&o)
}
}
检查清单
相关技能
外部资源