Functional Options 是 Go 中处理多可选参数初始化的唯一合理方案,必须用变参签名func NewClient(opts ...Option) Client、Option类型为func(T)、默认值初始化在apply前完成、禁止副作用且字段需导出。

Functional Options 是 Go 里解决多可选参数初始化的唯一合理路径,不是“可选方案”,而是“别无选择”。 其他方式要么破坏兼容性(加个字段就得改所有调用),要么语义模糊(结构体传参分不清 0 是用户设的还是没设),要么维护爆炸(Builder 模式要写一堆类型和方法)。
func NewClient(opts Option) *Client 签名必须带 ...
常见错误是把构造函数写成 func NewClient(opt Option) *Client —— 它只收一个 Option 值,而你调用时传了 NewClient(WithTimeout(5), WithRetries(3)),Go 不会自动打包多个函数为切片,直接报错:cannot use WithTimeout(5) (value of type Option) as type Option in argument to NewClient。
正确签名必须是变参形式:func NewClient(opts ...Option) *Client。注意末尾的 ...,少它一个字符就编译不过。
内部必须用循环遍历:
立即学习“go语言免费学习笔记(深入)”;
func NewClient(opts ...Option) *Client {
c := &Client{
timeout: 30 * time.Second,
retries: 3,
}
for _, opt := range opts {
opt(c)
}
return c
}不能只写 if len(opts) > 0 { opts[0](c) } —— 否则第二个选项静默丢弃,极难调试。
Option 必须是 func(*T),不能是值传递
定义成 type Option func(Client) 或 func(Config) Config 看似能跑,但所有字段修改都发生在副本上,最终对象仍是默认值。你会看到 WithTimeout 像没起作用,又不报错,调试时非常痛苦。
真正生效的前提只有一个:Option 函数体内操作的是原始内存地址。所以:
-
WithTimeout返回的闭包,参数必须是*Client,不是Client - 构造函数里初始化结构体后,必须传指针给每个
opt(c),比如c := &Client{},而不是c := Client{} - 所有
WithXXX工厂函数返回的匿名函数,签名必须匹配func(*Client)
默认值初始化必须在 apply options 之前
默认值不是兜底逻辑,而是配置起点。顺序错了,用户明确传的配置会被覆盖回去。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
典型翻车代码:
c := &Client{}
opt(c)
c.timeout = 30 * time.Second哪怕用户调了 WithTimeout(10 * time.Second),最终 timeout 还是 30 秒。
正确做法是:所有字段(包括 map、slice、指针)都得在 &Client{} 这一步完成合理初始化,例如:
headers: make(map[string]string)middleware: []Middleware{}tlsCfg: &tls.Config{}
否则后续对 nil map/slice 的写入会 panic。
WithXXX 函数里禁止副作用
WithTimeout、WithLogger 这类函数语义是“声明配置”,不是“执行初始化”。一旦混入副作用,实例行为就不可预测,尤其在测试或热重载时出问题。
禁止以下行为:
- 启动 goroutine(如在
WithContext里监听ctx.Done()) - 阻塞 IO(如
WithCertPath里直接ioutil.ReadFile) - 修改全局状态(如
WithLogger调log.SetOutput) - 深拷贝或分配大内存(如接收
*tls.Config却不 clone,外部改它会影响实例)
这些动作该延迟到 Client.Start() 或首次 Do() 时再做。
最易被忽略的一点:结构体字段为私有(unexported)时,WithXXX 函数无法赋值——必须确保字段首字母大写,且 Option 函数能访问到它们。这不是语法限制,而是封装边界问题;若真要隐藏字段,就得靠组合+接口,而不是靠函数选项模式硬扛。

















