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

4.2 KiB
Raw Blame History

函数式选项 vs 配置结构体

来源:Google 风格指南, Uber 风格指南

函数式选项和配置结构体解决相同的问题 — 构造函数的可选配置 — 但它们有不同的权衡。根据 API 受众、可扩展性需求和复杂性预算来选择。

决策框架

需要可选配置?
├─ 内部或仅测试 API?
│   └─ 配置结构体(更简单,更少样板代码)
├─ 具有 3 个以上选项的公共 API?
│   └─ 函数式选项(可扩展,向后兼容)
├─ 选项需要校验或有相互依赖?
│   └─ 函数式选项(在 apply 或构造函数中校验)
├─ 所有选项通常一起指定?
│   └─ 配置结构体(一个字面量,无需 With* 仪式)
└─ 选项可能随时间增长?
    └─ 函数式选项(添加 With* 不会破坏调用者)

配置结构体模式

配置结构体将可选参数分组为传递给构造函数的单个结构体。零值作为默认值,或提供 DefaultConfig()。

好

type Config struct {
    Timeout  time.Duration // 零 = 无超时
    MaxRetry int           // 零 = 无重试
    Logger   *log.Logger   // nil = 丢弃
}

func NewClient(addr string, cfg Config) *Client {
    if cfg.Logger == nil {
        cfg.Logger = log.New(io.Discard, "", 0)
    }
    return &Client{addr: addr, cfg: cfg}
}
c := NewClient("localhost:8080", Config{
    Timeout:  5 * time.Second,
    MaxRetry: 3,
})

不好 — 在公共 API 中依赖未导出的配置字段:

type config struct {  // 未导出:调用者无法构造
    timeout time.Duration
}

func NewClient(addr string, cfg config) *Client { ... }

当零值不适用时

如果零是一个有效的非默认值(例如,超时为 0 表示"无超时",但期望的默认值是 30s),使用指针字段或哨兵值:

type Config struct {
    Timeout *time.Duration // nil = 使用默认值(30s),零 = 无超时
}

比较

方面 函数式选项 配置结构体
样板代码 高(每个选项需要类型 + apply + With*) 低(一个结构体)
可扩展性 添加 With* — 无破坏性变更 添加字段 — 无破坏性变更
向后兼容 对公共 API 极好 好(新字段获得零值)
默认值 内置于构造函数 零值或 DefaultConfig()
校验 在 apply 或构造函数循环中 在接收到结构体后的构造函数中
可发现性 With* 函数出现在 godoc 中 所有字段在一个结构体中可见
可测试性 比较选项或测试构造函数输出 比较结构体字面量
调用者体验 只指定与默认值不同的部分 必须构造结构体字面量
零值歧义 无 — 未设置的选项不应用 可能需要指针字段

何时优先使用配置结构体

  • 内部 API — 更少的仪式,在调用处更易读
  • 少量选项(1-3 个) — 函数式选项的开销不值得
  • 所有选项通常一起设置 — 可变参数风格没有好处
  • 不需要校验 — 简单的字段赋值即可
  • 选项是数据而非行为 — 结构体字段自然映射
srv := NewServer(Config{
    Port:    8080,
    TLSCert: "/path/to/cert.pem",
    TLSKey:  "/path/to/key.pem",
})

何时优先使用函数式选项

  • 公共/库 API — 调用者不应跟踪内部配置的演变
  • 3 个以上选项,每个都是可选的
  • 复杂默认值 — 默认值计算依赖于其他选项
  • 按选项校验 — 在 apply 时拒绝无效值
  • 选项可能增长 — 新的 With* 函数是纯粹增量的
srv := NewServer(
    WithPort(8080),
    WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
    WithLogger(logger),
)

混合方法

对于同时需要便利性和可扩展性的 API,接受配置结构体用于常见设置,函数式选项用于高级覆盖:

func NewServer(cfg Config, opts ...Option) *Server {
    s := &Server{cfg: cfg}
    for _, o := range opts {
        o.apply(&s.cfg)
    }
    return s
}

谨慎使用 — 它增加了复杂性。每个 API 优先使用一种方法。