Builder 模式在 Go 中必须用指针接收者、Build() 集中校验与默认填充、深拷贝可变字段、禁止复用实例,因其无编译期强制赋值,且需严守纯数据构造边界。

Builder 模式在 Go 里不是靠“语言学习技巧”实现的,而是靠结构体设计、指针接收者和运行时校验硬刚出来的。想靠语法糖或反射偷懒,基本会掉坑里。
为什么不能用值接收者写 WithName()
值接收者方法(func (b UserBuilder) WithName(name string) UserBuilder)每次调用都操作副本,字段根本没改。链式调用看着流畅,实际构建出的 User 全是零值。
- 必须用指针接收者:
func (b *UserBuilder) WithName(name string) *UserBuilder - 返回
*UserBuilder,不是新分配的实例——否则链断了 - 如果误用值接收者,
Build()返回的User字段全为空,且无编译错误,调试时才发现
Build() 里不做校验等于没建
Go 不支持编译期强制赋值,Build() 是唯一能集中做必填检查、互斥判断、默认填充的地方。把校验塞进 WithName() 会导致链式调用中途 panic,用户没法“只设部分字段再 later Build()”。
- 用标记字段区分“未设置”和“设为空”,比如
nameSet bool,而不是仅靠name == "" - 互斥字段检查放
Build():如if b.token != "" && b.username != "" { return nil, errors.New("token and basic auth conflict") } - 默认值也延迟到
Build()填充:if b.timeout == 0 { b.timeout = 30 * time.Second }
含切片、map、指针字段时,Build() 必须深拷贝
直接 return &User{Roles: b.roles} 会让外部修改 b.roles 影响已构建对象。Go 的切片 header 是共享的,浅拷贝等于暴露内部状态。
立即学习“go语言免费学习笔记(深入)”;
-
Roles是[]string?用append([]string{}, b.roles...) -
Headers是map[string]string?用copyMap(b.headers)手动遍历复制 - 别存
*sql.DB或http.Client这类可变对象指针——Builder 应该是纯数据容器,不是资源管理器
并发场景下复用 *UserBuilder 实例极危险
Builder 不是线程安全的。多个 goroutine 同时调用同一个 *UserBuilder 的 WithName() 和 Build(),字段会被踩,Build() 可能返回半中间状态的对象。
- 每次构建都应调用
NewUserBuilder()获得新实例 - 不要把 Builder 当单例或池化对象使用
- 如果真要复用逻辑(比如 AdminUserBuilder 复用 UserBuilder),用组合:
type AdminUserBuilder struct { *UserBuilder; adminPrivileges []string },但每个AdminUserBuilder实例仍需独立创建
Builder 的复杂点不在语法,而在状态管理边界:它得清楚自己只管“构造”,不管“运行”;只存数据,不存资源;所有副作用(网络、IO、锁)必须排除在 Build() 之外。这点容易被忽略,一不小心就把 Builder 写成了初始化器 + 初始化后钩子的混合体。


















