option类型必须是func(*t),不是struct或interface;必选参数须放构造函数签名前,withxxx函数仅纯赋值、禁io/panic,默认值初始化必须在遍历opts前完成。

Option 类型必须是 func(*T),不是 struct 也不是 interface
定义 type Option func(*Client) 是唯一稳妥的起点。用 struct 或 interface 包裹配置项,会立刻失去类型安全、IDE 跳转和编译期检查能力——比如拼错 "timeot" 字段名,map 方式到运行时才 panic;interface 实现会让 WithTimeout 失去函数签名推导,调用时无法自动补全。
常见错误包括:
- 定义
type Option interface{ Apply(*Client) }:导致构造函数无法直接接收多个选项,且无法内联展开 - 定义
type Option struct{ Timeout time.Duration }:本质上退化为数据容器,校验逻辑得分散在每个字段赋值处,无法统一控制执行时机 - 把
WithXxx写成返回新*Client:那就不是选项模式,而是工厂函数,破坏了“原地配置”的语义一致性
必选参数放签名前,可选参数才进 Option 列表
别把所有东西都塞进 Option。像 addr string、host string 这类没有合理默认值、缺失即不可用的字段,必须作为构造函数显式参数放在最前面。
例如:func NewClient(addr string, opts ...Option) *Client —— 这样漏传 addr 编译直接报错,而不是等到 c.Do() 才 panic。
容易踩的坑:
- 用
nil判断是否设置:比如if c.tlsCfg == nil就认为用户没配 TLS,但用户可能就是想禁用 TLS,此时nil是合法值 - 把关键默认值逻辑藏在某个
WithXXX里:比如WithTimeout里悄悄设c.retries = 3,会导致不传它时 retries 为 0,行为不可控 - 误以为
...Option能覆盖全部需求:结果连地址都没传,程序启动就崩溃
WithXxx 函数必须只改字段,禁止 IO、goroutine、panic
选项函数的语义是“声明式配置”,不是“初始化动作”。所有副作用(如 dial 连接、读证书文件、启动监听 goroutine)都该延迟到对象真正被使用时,比如 Client.Do() 或 Server.Start() 阶段。
否则会出现:
- 构造阶段阻塞:比如
WithTLSConfig里同步读磁盘,NewClient 就卡住 - 资源提前泄漏:闭包捕获了
*http.Client,但用户根本没调用后续方法,连接池就一直占着内存 - 顺序依赖难维护:两个选项都启动 goroutine,谁先谁后?竞态风险没人管
正确写法示例:func WithTimeout(d time.Duration) Option { return func(c *Client) { c.timeout = d } } —— 纯赋值,无分支,无调用,无等待。
默认值初始化必须在遍历 opts 前完成
结构体字段的默认值必须写死在 c := &Client{...} 这一行,不能靠某个 WithXXX 补全。否则一旦用户没传那个选项,字段就是零值,而零值在很多场景下(如 timeout = 0)并不等价于“未设置”。
典型错误顺序:
- 先
for _, opt := range opts { opt(c) },再设默认值 → 后者永远覆盖不了前者 - 在某个
WithBaseURL里顺手设c.timeout = 30 * time.Second→ 用户传了WithTimeout(5)又被覆盖,行为不可预测 - 校验逻辑插在中间:比如
opt(c)后立刻if c.timeout → 用户还没传 <code>WithTimeout,但默认值还没设,校验就炸了
标准流程只有三步:创建带默认值的实例 → 逐个 apply opts → 最后统一校验最终状态。多一步少一步都会出问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











