Skip to content

建造者模式

建造者模式(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 对比

维度链式 BuilderFunctional 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-gogoogle-api-go-clientprometheus 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 里同样常用,且实现非常轻量。