在Golang项目里,当我们需要创建一个字段很多、且大部分可选的对象时,传统的构造函数会变得越来越难用。建造者模式把对象的创建步骤拆开,让调用方以更清晰的方式逐步赋值,最后统一生成目标实例。下面先看一张示意图。

为什么需要建造者模式
假设有一个服务器配置结构体,包含地址、端口、超时时间、最大连接数、是否开启TLS等十几个字段。如果只用NewServer(addr string, port int, timeout time.Duration, ...)这样的函数,调用者必须记住每个参数的位置,一旦顺序写错,编译器不会报错但运行行为完全错误。更麻烦的是,很多字段是可选的,为了覆盖不同组合,开发者往往会写出七八个不同签名的构造函数,代码维护成本陡增。
建造者模式的核心思想是:不直接暴露结构体的完整构造过程,而是提供一个独立的构建器(Builder),它负责暂存每一步设置的值,在真正构建时再做校验与组装。这样调用方写出来的代码是连贯且自解释的,例如NewServerBuilder().WithAddr("127.0.0.1").WithPort(8080).Build(),每一个步骤都明确表达了意图。
基础实现方式
在Golang中,我们通常会定义一个目标结构体和一个对应的builder结构体。builder结构体持有目标对象的指针或值,并对外提供一系列WithXxx方法,这些方法返回builder自身的指针,从而支持链式调用。最后的Build方法会执行必要的校验,并返回构建好的对象或错误。
下面是一个最直观的示例,展示如何为一个HTTP客户端配置实现建造者:
package main
import (
"errors"
"time"
)
// HttpClient 是我们要构建的目标对象
type HttpClient struct {
timeout time.Duration
maxConn int
baseURL string
retry int
}
// HttpClientBuilder 是构建器
type HttpClientBuilder struct {
client HttpClient
}
// NewHttpClientBuilder 创建构建器实例
func NewHttpClientBuilder() *HttpClientBuilder {
return &HttpClientBuilder{
client: HttpClient{
timeout: 5 * time.Second,
maxConn: 10,
retry: 0,
},
}
}
// WithBaseURL 设置基础地址
func (b *HttpClientBuilder) WithBaseURL(url string) *HttpClientBuilder {
b.client.baseURL = url
return b
}
// WithTimeout 设置超时时间
func (b *HttpClientBuilder) WithTimeout(t time.Duration) *HttpClientBuilder {
b.client.timeout = t
return b
}
// WithMaxConn 设置最大连接数
func (b *HttpClientBuilder) WithMaxConn(n int) *HttpClientBuilder {
b.client.maxConn = n
return b
}
// WithRetry 设置重试次数
func (b *HttpClientBuilder) WithRetry(n int) *HttpClientBuilder {
b.client.retry = n
return b
}
// Build 执行校验并返回对象
func (b *HttpClientBuilder) Build() (*HttpClient, error) {
if b.client.baseURL == "" {
return nil, errors.New("baseURL不能为空")
}
if b.client.maxConn <= 0 {
return nil, errors.New("maxConn必须大于0")
}
return &b.client, nil
}
上面的代码把默认值放在NewHttpClientBuilder里初始化,调用方只需覆盖关心的值。由于每个WithXxx都返回*HttpClientBuilder,所以我们可以写出链式表达式,阅读起来像在描述对象的属性而不是在填参数表。
这种写法的优点是构建逻辑集中,所有合法性检查都在Build里完成,不会出现半成品对象被外部直接使用的情况。缺点是需要额外写builder结构体和方法,对于极简单的对象反而显得啰嗦。
与函数选项模式对比
Golang社区另一种常见做法是函数选项模式(Functional Options)。它定义一系列func(*Target)类型的函数,在构造函数里遍历应用。下面用同样的HttpClient演示:
package main
import "time"
type HttpClient struct {
timeout time.Duration
maxConn int
baseURL string
}
type Option func(*HttpClient)
func WithBaseURL(url string) Option {
return func(c *HttpClient) {
c.baseURL = url
}
}
func WithTimeout(t time.Duration) Option {
return func(c *HttpClient) {
c.timeout = t
}
}
func NewHttpClient(opts ...Option) *HttpClient {
c := &HttpClient{
timeout: 5 * time.Second,
maxConn: 10,
}
for _, opt := range opts {
opt(c)
}
return c
}
函数选项模式代码量更少,而且天然支持扩展,新增选项不需要改动构造函数签名。但它不容易在中间步骤做阶段性校验,而且所有选项应用完之后如果要做整体校验,仍需在NewHttpClient末尾处理。建造者模式则把校验固化在Build方法,语义上更强调“构建完成”这一动作。
实际选型时,如果对象字段多且必填项与可选参杂、希望在构建时统一报错,建造者更合适;如果只是简单配置且追求极简API,函数选项模式更轻巧。两者并不是互斥的,有些库会先用选项函数收集配置,再在内部用builder组装复杂依赖。
使用接口隐藏构建细节
当构建步骤很多时,我们还可以把builder抽象成接口,让不同实现对应不同构建策略。例如基础版和完整版客户端用同一套步骤但默认值不同。下面给出一个极简接口示例:
package main
type Builder interface {
WithBaseURL(string) Builder
WithTimeout(int) Builder
Build() (map[string]int, error)
}
type simpleBuilder struct {
url string
second int
}
func (s *simpleBuilder) WithBaseURL(u string) Builder {
s.url = u
return s
}
func (s *simpleBuilder) WithTimeout(sec int) Builder {
s.second = sec
return s
}
func (s *simpleBuilder) Build() (map[string]int, error) {
if s.url == "" {
return nil, nil
}
return map[string]int{"timeout": s.second}, nil
}
接口化之后,调用方只依赖Builder类型,具体怎么存数据、怎么校验都由实现决定。这在写单元测试时特别有用,可以用伪实现替换真实构建逻辑。不过接口方式会引入一层抽象,小项目里直接用具体builder结构体反而更好读。
总体来看,Golang实现建造者模式没有语言层面的语法糖,靠的是指针接收者返回自身和明确的Build收尾。只要遵循“步骤可链、构建集中校验”的原则,就能写出既灵活又安全的对象生成代码。
Golang建造者模式builder_pattern修改时间:2026-08-03 08:00:32