Appearance
建造者模式
建造者模式(Builder)的意图是:把一个复杂对象的构造与它的表示分离,使得同样的构造过程可以创建不同的表示。在 Java 里,这通常表现为一个 Builder 内部类,链式调用一系列 setter 后调用 build()。在 Go 里,链式 Builder 依然适用,但更流行的是一种叫 Functional Options(函数式选项)的写法,它几乎成了 Go 的「标志性建造者」。
本章我们讲两种实现:链式 Builder 和 Functional Options,并对比它们的适用场景。
一、建造者模式意图:分离构造与表示
1. 问题背景
假设你要构造一个 HTTP 客户端,它有很多可选参数:
go
type HTTPClient struct {
URL string
Timeout time.Duration
RetryCount int
Headers map[string]string
AuthToken string
InsecureSSL bool
}直接构造会有几个痛点:
- 参数太多:
NewHTTPClient(url, timeout, retry, headers, token, ssl)一长串,调用方记不住顺序。 - 默认值难处理:很多参数想用默认值,但你不知道传 0 还是传 -1 才算「用默认」。
- 扩展难:以后加一个参数,所有调用处都要改签名。
建造者模式解决这些问题:用一组方法分步设置参数,最后一次性构造。
2. 适用场景
- 构造对象有较多可选参数(一般 4 个以上)。
- 需要合理的默认值。
- 构造过程分多步,或有校验逻辑。
- 同一组参数可能产生不同配置的对象。
二、Go 实现:链式调用(Builder Pattern)
1. 基本结构
链式 Builder 的核心:每个 setter 方法返回 *Builder 自身,从而可以连续调用;最后 Build() 返回目标对象。
go
package main
import (
"fmt"
"time"
)
type HTTPClient struct {
URL string
Timeout time.Duration
RetryCount int
Headers map[string]string
AuthToken string
InsecureSSL bool
}
// Builder 负责分步构造 HTTPClient
type HTTPClientBuilder struct {
url string
timeout time.Duration
retryCount int
headers map[string]string
authToken string
insecureSSL bool
}
// NewBuilder 是构造 Builder 的入口
func NewBuilder(url string) *HTTPClientBuilder {
return &HTTPClientBuilder{
url: url,
timeout: 30 * time.Second, // 默认值
retryCount: 3, // 默认值
headers: map[string]string{},
}
}
// 每个 setter 返回 *Builder,实现链式调用
func (b *HTTPClientBuilder) WithTimeout(t time.Duration) *HTTPClientBuilder {
b.timeout = t
return b
}
func (b *HTTPClientBuilder) WithRetry(n int) *HTTPClientBuilder {
b.retryCount = n
return b
}
func (b *HTTPClientBuilder) WithHeader(k, v string) *HTTPClientBuilder {
b.headers[k] = v
return b
}
func (b *HTTPClientBuilder) WithAuthToken(t string) *HTTPClientBuilder {
b.authToken = t
return b
}
func (b *HTTPClientBuilder) WithInsecureSSL(v bool) *HTTPClientBuilder {
b.insecureSSL = v
return b
}
// Build 完成构造,返回最终对象
func (b *HTTPClientBuilder) Build() (*HTTPClient, error) {
if b.url == "" {
return nil, fmt.Errorf("url 不能为空")
}
return &HTTPClient{
URL: b.url,
Timeout: b.timeout,
RetryCount: b.retryCount,
Headers: b.headers,
AuthToken: b.authToken,
InsecureSSL: b.insecureSSL,
}, nil
}
func main() {
c, err := NewBuilder("https://api.example.com").
WithTimeout(10 * time.Second).
WithRetry(5).
WithHeader("User-Agent", "myapp/1.0").
WithAuthToken("Bearer xxx").
WithInsecureSSL(false).
Build()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", c)
}链式 Builder 的优点:
- 可读性好,每个参数有名字。
- 默认值在
NewBuilder里集中设置。 - 可选参数随意组合,没设置的用默认值。
Build可以做校验,返回 error。
2. 不可变 Builder 变体
上面的 Builder 是可变的(每次修改自己)。如果你希望每次 WithXxx 都返回一个新的 Builder(更函数式),可以这样:
go
package main
import "fmt"
type Server struct {
Host string
Port int
}
type ServerBuilder struct {
host string
port int
}
func (b ServerBuilder) WithHost(h string) ServerBuilder {
b.host = h
return b
}
func (b ServerBuilder) WithPort(p int) ServerBuilder {
b.port = p
return b
}
func (b ServerBuilder) Build() Server {
return Server{Host: b.host, Port: b.port}
}
func main() {
s := ServerBuilder{}.
WithHost("0.0.0.0").
WithPort(8080).
Build()
fmt.Printf("%+v\n", s)
}这种写法每次返回值类型(不是指针)的 Builder,链式调用是「复制 + 修改」,原始 Builder 不变。代价是每次多一次拷贝,但更安全。Go 标准库和社区都更倾向指针版本(性能好、惯用法),这里仅供参考。
三、函数式选项模式(Functional Options)
Functional Options 是 Go 社区最流行的「建造者」实现,由 Dave Cheney 和 Rob Pike 等人推广。它的核心思想:把「设置某个字段」封装成一个函数,通过 ...Option 可变参数传入构造函数。
1. 基本结构
go
package main
import (
"fmt"
"time"
)
// === 目标对象 ===
type Server struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
}
// === Option 类型:一个修改 Server 的函数 ===
type Option func(*Server)
// === 构造函数:必填参数显式传,可选参数用 Option ===
func NewServer(host string, port int, opts ...Option) *Server {
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second, // 默认值
maxConn: 100, // 默认值
tls: false,
}
for _, opt := range opts {
opt(s)
}
return s
}
// === 各个 WithXxx 选项函数 ===
func WithTimeout(t time.Duration) Option {
return func(s *Server) { s.timeout = t }
}
func WithMaxConn(n int) Option {
return func(s *Server) { s.maxConn = n }
}
func WithTLS(v bool) Option {
return func(s *Server) { s.tls = v }
}
func main() {
s1 := NewServer("0.0.0.0", 8080)
fmt.Printf("默认: %+v\n", s1)
s2 := NewServer("0.0.0.0", 443,
WithTimeout(10*time.Second),
WithMaxConn(500),
WithTLS(true),
)
fmt.Printf("自定义: %+v\n", s2)
}2. 为什么 Functional Options 这么受欢迎
- API 稳定:以后加新选项只是加一个
WithXxx函数,构造函数签名不变,不破坏调用方。 - 默认值集中:所有默认值在
NewServer里设置,一目了然。 - 可读性强:
NewServer(host, port, WithTimeout(...))自描述。 - 可选参数灵活:调用方只写关心的选项,其余用默认。
- 可组合:选项可以打包成一个变量复用,比如
dbOpts := []Option{WithTimeout(...), WithMaxConn(...)}。 - 支持校验:选项函数里可以写校验逻辑,甚至可以返回 error(用专门的
Option类型包装)。
3. 带错误的选项
如果选项需要校验(比如 timeout 不能为负),可以用返回 error 的变体:
go
package main
import (
"errors"
"fmt"
"time"
)
type Config struct {
timeout time.Duration
retry int
}
type Option func(*Config) error
func WithTimeout(t time.Duration) Option {
return func(c *Config) error {
if t < 0 {
return errors.New("timeout 不能为负")
}
c.timeout = t
return nil
}
}
func WithRetry(n int) Option {
return func(c *Config) error {
if n < 0 {
return errors.New("retry 不能为负")
}
c.retry = n
return nil
}
}
func NewConfig(opts ...Option) (*Config, error) {
c := &Config{
timeout: 30 * time.Second,
retry: 3,
}
for _, opt := range opts {
if err := opt(c); err != nil {
return nil, fmt.Errorf("应用选项失败: %w", err)
}
}
return c, nil
}
func main() {
c, err := NewConfig(WithTimeout(-1 * time.Second))
if err != nil {
fmt.Println("错误:", err)
}
c2, err := NewConfig(WithTimeout(5*time.Second), WithRetry(10))
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("成功: %+v\n", c2)
}4. 默认值处理的细节
默认值通常有两种处理方式:
方式 A:构造函数里设默认,选项覆盖
go
func NewServer(...) *Server {
s := &Server{timeout: 30 * time.Second} // 默认
for _, opt := range opts { opt(s) } // 选项覆盖
return s
}方式 B:选项内部判断「零值则设默认」
go
func WithTimeout(t time.Duration) Option {
return func(s *Server) {
if t == 0 {
s.timeout = 30 * time.Second
} else {
s.timeout = t
}
}
}方式 A 更简单清晰,是主流做法。方式 B 的好处是「不传这个选项」和「传零值」都能得到默认值,但代码啰嗦。
四、链式 Builder vs Functional Options 对比
| 维度 | 链式 Builder | Functional Options |
|---|---|---|
| 可读性 | NewBuilder().WithA().WithB().Build() | NewX(a, b, WithA(), WithB()) |
| 必填参数 | 较难强制(除非拆成 NewBuilder 必填 + setter 可选) | 容易:必填作为函数显式参数 |
| 默认值 | 在 NewBuilder 里设置 | 在 NewX 里设置 |
| 错误处理 | Build() 返回 error 自然 | 需要变体(Option 返回 error)或后续校验 |
| API 稳定性 | 加参数加 setter 即可 | 加参数加 WithX 即可 |
| 可组合性 | 较弱(Builder 是状态化的) | 强([]Option 可拼接) |
| Go 惯用度 | 中等 | 极高 |
| 适合场景 | 步骤多、有强校验、构造过程分阶段 | 选项多、API 稳定、构造简单 |
经验法则:
- 如果构造过程分多个阶段、有复杂校验、需要返回 error → 链式 Builder。
- 如果只是「一堆可选参数 + 默认值」 → Functional Options。
- 如果是公开 API、希望长期稳定 → Functional Options(更符合 Go 习惯)。
- 如果参数有强依赖(A 必须在 B 之前设置) → 链式 Builder 更容易表达顺序。
五、标准库中的应用:grpc.Dial 的 DialOption
gRPC 的 grpc.Dial 是 Functional Options 最著名的实践。它的签名是:
go
func Dial(target string, opts ...DialOption) (*ClientConn, error)DialOption 内部是一个结构体,但对外表现为 WithXxx 函数:
go
grpc.Dial("localhost:50051",
grpc.WithInsecure(),
grpc.WithTransportCredentials(creds),
grpc.WithUnaryInterceptor(...),
grpc.WithTimeout(10*time.Second),
)这种设计让 gRPC 客户端的配置非常灵活,而且新版本加选项完全不破坏老代码。除 gRPC 外,aws-sdk-go、google-api-go-client、prometheus client 等几乎都用 Functional Options。
六、实战示例
1. HTTP 客户端构建器(Functional Options 版)
go
package main
import (
"fmt"
"time"
)
type HTTPClient struct {
url string
timeout time.Duration
retry int
headers map[string]string
token string
}
type Option func(*HTTPClient)
func NewHTTPClient(url string, opts ...Option) *HTTPClient {
c := &HTTPClient{
url: url,
timeout: 30 * time.Second,
retry: 3,
headers: map[string]string{},
}
for _, opt := range opts {
opt(c)
}
return c
}
func WithTimeout(t time.Duration) Option {
return func(c *HTTPClient) { c.timeout = t }
}
func WithRetry(n int) Option {
return func(c *HTTPClient) { c.retry = n }
}
func WithHeader(k, v string) Option {
return func(c *HTTPClient) { c.headers[k] = v }
}
func WithToken(t string) Option {
return func(c *HTTPClient) { c.token = t }
}
// 选项可以组合复用
func ProductionDefaults() Option {
return func(c *HTTPClient) {
c.timeout = 5 * time.Second
c.retry = 5
c.headers["Env"] = "prod"
}
}
func main() {
// 复用一组选项
prodOpts := []Option{
ProductionDefaults(),
WithToken("prod-token"),
WithHeader("User-Agent", "prod/1.0"),
}
c1 := NewHTTPClient("https://api.prod.com", prodOpts...)
c2 := NewHTTPClient("https://api.prod.com/v2", prodOpts...)
fmt.Printf("c1: %+v\n", c1)
fmt.Printf("c2: %+v\n", c2)
}注意 ProductionDefaults() 这种「打包选项」的写法——这是 Functional Options 独有的优势,链式 Builder 很难做到这么优雅。
2. 数据库配置构建器(链式 Builder 版)
go
package main
import (
"errors"
"fmt"
"time"
)
type DBConfig struct {
Driver string
DSN string
MaxOpen int
MaxIdle int
MaxLife time.Duration
ReadOnly bool
}
type DBConfigBuilder struct {
config DBConfig
err error
}
func NewDBConfigBuilder(driver, dsn string) *DBConfigBuilder {
b := &DBConfigBuilder{}
if driver == "" {
b.err = errors.New("driver 不能为空")
return b
}
if dsn == "" {
b.err = errors.New("dsn 不能为空")
return b
}
b.config = DBConfig{
Driver: driver,
DSN: dsn,
MaxOpen: 20,
MaxIdle: 5,
MaxLife: 30 * time.Minute,
}
return b
}
func (b *DBConfigBuilder) WithMaxOpen(n int) *DBConfigBuilder {
if b.err != nil {
return b
}
if n <= 0 {
b.err = errors.New("maxOpen 必须 > 0")
return b
}
b.config.MaxOpen = n
return b
}
func (b *DBConfigBuilder) WithMaxIdle(n int) *DBConfigBuilder {
if b.err != nil {
return b
}
if n < 0 {
b.err = errors.New("maxIdle 不能为负")
return b
}
b.config.MaxIdle = n
return b
}
func (b *DBConfigBuilder) WithMaxLife(d time.Duration) *DBConfigBuilder {
if b.err != nil {
return b
}
b.config.MaxLife = d
return b
}
func (b *DBConfigBuilder) WithReadOnly(v bool) *DBConfigBuilder {
if b.err != nil {
return b
}
b.config.ReadOnly = v
return b
}
func (b *DBConfigBuilder) Build() (*DBConfig, error) {
if b.err != nil {
return nil, b.err
}
return &b.config, nil
}
func main() {
cfg, err := NewDBConfigBuilder("mysql", "root:@tcp(localhost:3306)/db").
WithMaxOpen(50).
WithMaxIdle(10).
WithMaxLife(time.Hour).
WithReadOnly(false).
Build()
if err != nil {
fmt.Println("构建失败:", err)
return
}
fmt.Printf("DB 配置: %+v\n", cfg)
// 错误演示
_, err = NewDBConfigBuilder("", "dsn").Build()
fmt.Println("预期错误:", err)
}这个例子展示了链式 Builder 的一个常见技巧:「错误延迟」。每个 setter 不立即 panic,而是把错误存在 Builder 里,最后 Build 一次性返回。这样调用方链式调用不会被中间错误打断,代码更流畅。
七、小结
- 建造者模式用于分步构造复杂对象,分离构造与表示,处理多可选参数和默认值。
- Go 有两种主流实现:链式 Builder(
b.WithA().WithB().Build())和 Functional Options(NewX(a, WithB(), WithC()))。 - 链式 Builder 适合多步骤、强校验、需要返回 error 的场景。
- Functional Options 是 Go 社区最爱,API 稳定、可组合、可读性强,适合「一堆可选参数 + 默认值」。
- Functional Options 的核心:
type Option func(*T),NewX(...Option)内部循环应用选项。 - 选项可以「打包」复用(如
ProductionDefaults()),这是 Functional Options 独有的优势。 - 标准库及知名项目(gRPC、AWS SDK、Prometheus)大量使用 Functional Options,是 Go 公共 API 设计的事实标准。
下一篇讲适配器与外观模式,这两个结构型模式在 Go 里同样常用,且实现非常轻量。