Files
OpenFlare/.agents/skills/go-functional-options/SKILL.md
T
2026-08-04 11:40:32 +08:00

6.1 KiB

name, description, license, metadata
name description license metadata
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). Apache-2.0
sources
Uber Style Guide

函数式选项模式

函数式选项是一种模式,你声明一个不透明的 Option 类型,在内部结构体中记录信息。构造函数接受可变数量的这些选项并将其应用于配置结果。

何时使用

在以下情况使用函数式选项:

  • 构造函数或公共 API 上有 3 个以上可选参数
  • 可扩展 API,可能随时间增加新选项
  • 良好的调用者体验很重要(无需传递默认值)

模式

核心组件

  1. 未导出的 options 结构体 - 保存所有配置
  2. 导出的 Option 接口 - 带有未导出的 apply 方法
  3. Option 类型 - 实现接口
  4. With* 构造函数 - 创建选项

Option 接口

type Option interface {
    apply(*options)
}

未导出的 apply 方法确保只能使用来自本包的选项。

完整实现

package db

import "go.uber.org/zap"

// options 保存打开连接的所有配置。
type options struct {
    cache  bool
    logger *zap.Logger
}

// Option 配置我们如何打开连接。
type Option interface {
    apply(*options)
}

// cacheOption 为缓存设置实现 Option(简单类型别名)。
type cacheOption bool

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

// WithCache 启用或禁用缓存。
func WithCache(c bool) Option {
    return cacheOption(c)
}

// loggerOption 为日志设置实现 Option(用于指针的结构体)。
type loggerOption struct {
    Log *zap.Logger
}

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

// WithLogger 设置连接的日志记录器。
func WithLogger(log *zap.Logger) Option {
    return loggerOption{Log: log}
}

// Open 创建一个连接。
func Open(addr string, opts ...Option) (*Connection, error) {
    // 从默认值开始
    options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
    }

    // 应用所有提供的选项
    for _, o := range opts {
        o.apply(&options)
    }

    // 使用 options.cache 和 options.logger...
    return &Connection{}, nil
}

使用示例

不使用函数式选项(不好)

// 调用者必须始终提供所有参数,即使是默认值
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)

使用函数式选项(好)

// 只在需要时提供选项
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 }
}

优先使用接口方法,因为:

  1. 可测试性 - 选项可以在测试和 mock 中进行比较
  2. 可调试性 - 选项可以实现 fmt.Stringer
  3. 灵活性 - 选项可以实现额外的接口
  4. 可见性 - 选项类型在文档中可见

快速参考

// 1. 带有默认值的未导出 options 结构体
type options struct {
    field1 Type1
    field2 Type2
}

// 2. 导出的 Option 接口,未导出的方法
type Option interface {
    apply(*options)
}

// 3. Option 类型 + apply + With* 构造函数
type field1Option Type1

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

// 4. 构造函数在默认值之上应用选项
func New(required string, opts ...Option) (*Thing, error) {
    o := options{field1: defaultField1, field2: defaultField2}
    for _, opt := range opts {
        opt.apply(&o)
    }
    // ...
}

检查清单

  • options 结构体未导出
  • Option 接口有未导出的 apply 方法
  • 每个选项有 With* 构造函数
  • 默认值在应用选项之前设置
  • 必需参数与 ...Option 分开

相关技能

  • 接口设计:在设计 Option 接口或选择接口与闭包方法时,参见 go-interfaces
  • 命名约定:在命名 With* 构造函数、选项类型或未导出的 options 结构体时,参见 go-naming
  • 函数设计:在组织文件中的构造函数或格式化可变参数签名时,参见 go-functions
  • 文档:在记录 Option 类型、With* 函数或构造函数行为时,参见 go-documentation

外部资源