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

130 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 函数式选项 vs 配置结构体
> **来源**:Google 风格指南, Uber 风格指南
函数式选项和配置结构体解决相同的问题 — 构造函数的可选配置 — 但它们有不同的权衡。根据 API 受众、可扩展性需求和复杂性预算来选择。
## 决策框架
```
需要可选配置?
├─ 内部或仅测试 API?
│ └─ 配置结构体(更简单,更少样板代码)
├─ 具有 3 个以上选项的公共 API?
│ └─ 函数式选项(可扩展,向后兼容)
├─ 选项需要校验或有相互依赖?
│ └─ 函数式选项(在 apply 或构造函数中校验)
├─ 所有选项通常一起指定?
│ └─ 配置结构体(一个字面量,无需 With* 仪式)
└─ 选项可能随时间增长?
└─ 函数式选项(添加 With* 不会破坏调用者)
```
## 配置结构体模式
配置结构体将可选参数分组为传递给构造函数的单个结构体。零值作为默认值,或提供 `DefaultConfig()`。
**好**
```go
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}
}
```
```go
c := NewClient("localhost:8080", Config{
Timeout: 5 * time.Second,
MaxRetry: 3,
})
```
**不好** — 在公共 API 中依赖未导出的配置字段:
```go
type config struct { // 未导出:调用者无法构造
timeout time.Duration
}
func NewClient(addr string, cfg config) *Client { ... }
```
### 当零值不适用时
如果零是一个有效的非默认值(例如,超时为 0 表示"无超时",但期望的默认值是 30s),使用指针字段或哨兵值:
```go
type Config struct {
Timeout *time.Duration // nil = 使用默认值(30s),零 = 无超时
}
```
## 比较
| 方面 | 函数式选项 | 配置结构体 |
|------|-----------|-----------|
| **样板代码** | 高(每个选项需要类型 + apply + With*) | 低(一个结构体) |
| **可扩展性** | 添加 `With*` — 无破坏性变更 | 添加字段 — 无破坏性变更 |
| **向后兼容** | 对公共 API 极好 | 好(新字段获得零值) |
| **默认值** | 内置于构造函数 | 零值或 `DefaultConfig()` |
| **校验** | 在 `apply` 或构造函数循环中 | 在接收到结构体后的构造函数中 |
| **可发现性** | `With*` 函数出现在 godoc 中 | 所有字段在一个结构体中可见 |
| **可测试性** | 比较选项或测试构造函数输出 | 比较结构体字面量 |
| **调用者体验** | 只指定与默认值不同的部分 | 必须构造结构体字面量 |
| **零值歧义** | 无 — 未设置的选项不应用 | 可能需要指针字段 |
## 何时优先使用配置结构体
- **内部 API** — 更少的仪式,在调用处更易读
- **少量选项(1-3 个)** — 函数式选项的开销不值得
- **所有选项通常一起设置** — 可变参数风格没有好处
- **不需要校验** — 简单的字段赋值即可
- **选项是数据而非行为** — 结构体字段自然映射
```go
srv := NewServer(Config{
Port: 8080,
TLSCert: "/path/to/cert.pem",
TLSKey: "/path/to/key.pem",
})
```
## 何时优先使用函数式选项
- **公共/库 API** — 调用者不应跟踪内部配置的演变
- **3 个以上选项**,每个都是可选的
- **复杂默认值** — 默认值计算依赖于其他选项
- **按选项校验** — 在 apply 时拒绝无效值
- **选项可能增长** — 新的 `With*` 函数是纯粹增量的
```go
srv := NewServer(
WithPort(8080),
WithTLS("/path/to/cert.pem", "/path/to/key.pem"),
WithLogger(logger),
)
```
## 混合方法
对于同时需要便利性和可扩展性的 API,接受配置结构体用于常见设置,函数式选项用于高级覆盖:
```go
func NewServer(cfg Config, opts ...Option) *Server {
s := &Server{cfg: cfg}
for _, o := range opts {
o.apply(&s.cfg)
}
return s
}
```
谨慎使用 — 它增加了复杂性。每个 API 优先使用一种方法。