最稳妥的选项模式是用 func(*T) 类型定义选项,每个 WithXxx 返回闭包修改接收者指针,构造函数遍历执行;结构体传参易错且难维护,map 缺乏类型安全,Option 必须是函数而非接口或 struct,需注意字段赋值、深拷贝及无副作用约束。

直接说结论:用 func(*T) 类型定义选项,每个 WithXxx 函数返回闭包修改接收者指针,构造函数统一遍历执行——这是 Go 里最稳妥、最易维护的选项模式写法。
为什么不能直接传结构体或 map
结构体字段一多,调用时就得填一堆零值占位,比如 NewClient("api.example.com", 0, 0, false, nil, ""),哪个 0 是超时、哪个是重试次数?根本分不清。更麻烦的是,加个新字段就得改所有调用点。
用 map[string]interface{} 看似灵活,但编译器拦不住拼错的键名(比如把 "timeout" 写成 "timeot"),IDE 补全失效,运行时 panic 才暴露问题。
Option 类型必须是函数,不是接口也不是 struct
常见错误是定义成 type Option interface{ Apply(*Client) } 或 type Option struct{ Timeout int }。前者失去类型推导和 IDE 跳转能力;后者本质是数据容器,没法做校验、无法链式组合,还容易漏传字段。
正确写法只有一行:
type Option func(*Client)
所有具体选项函数都必须匹配这个签名:
-
WithTimeout接收*Client并原地改c.timeout,不返回新对象 -
WithLogger同样只改c.logger,不检查 logger 是否为 nil(那是Apply或运行时的事) - 别在闭包里捕获大对象(比如整个
*http.Client),防止意外内存泄漏
构造函数里怎么安全 apply 多个 Option
签名必须支持变参:
func NewClient(opts ...Option) *Client
内部逻辑分三步:
立即学习“go语言免费学习笔记(深入)”;
- 先初始化默认值:
c := &Client{timeout: 30 * time.Second, retries: 3} - 再遍历
opts,逐个调用:for _, opt := range opts { opt(c) } - 如果某个 option 可能失败(比如校验证书路径),就让它返回
error,构造函数里必须检查:if err := opt(c); err != nil { return nil, err }
注意:顺序决定覆盖结果。写 NewClient(WithTimeout(10*time.Second), WithTimeout(2*time.Second)),最终生效的是 2 秒——这不是 bug,是设计使然。有隐含依赖的选项(如 WithTLSConfig 和 WithInsecureSkipVerify),得靠文档说明,代码里不强制校验。
切片、map、指针字段最容易出错
这些字段不能直接赋值,否则会覆盖原有内容:
- 给中间件切片赋值:
c.middlewares = mw❌ → 应该用c.middlewares = append(c.middlewares, mw...)✅ - 给 header map 赋值:
c.headers = h❌ → 必须先判空:if c.headers == nil { c.headers = make(map[string]string) }✅ - 传入
*tls.Config时,若需隔离修改,得深拷贝,不能直接c.tls = tlsCfg,否则外部改它会影响当前实例
Functional Options 的核心约束就一条:纯配置,无副作用。任何 IO、goroutine、资源预分配,都该挪到 Start()、Do() 这类方法里,而不是塞进 WithXxx 闭包中。


















