Golang 构造函数,函数选项和建造者模式

比较两种模式:FOP vs. Builder -选择哪一个?

image.png|300

注:本文核心内容由大语言模型生成,辅以人工事实核查与结构调整。

Go 的设计哲学优先考虑 简洁直接。虽然简单的结构体字面量足以应对基础的数据结构,但在构建复杂系统时,需要一种更可控的初始化过程来 强制执行规则管理依赖。这种需求推动了强大初始化模式的使用,其中包括惯用的 工厂函数 和流行的 函数选项模式(Functional Options Pattern, FOP)

Go 构造函数模式(惯用工厂函数)

在 Go 中,并没有像许多面向对象语言那样内置的“构造函数”语法。取而代之,开发者采用了一种惯用的设计模式:工厂函数,通常命名为 NewNewXXX

工厂函数的核心职责是 封装创建逻辑 并返回一个特定的、可立即使用的实例。

工厂函数的力量

当简单的结构体字面量初始化无法满足复杂需求时,工厂函数就显得至关重要。它提供了以下几个关键优势:

  1. 防止无效的零值: 尽管 Go 的零值机制确保变量在声明时总有一个已知的初始状态,但这个零值可能在逻辑上是无效的(例如,必填字段 Name 的空字符串)。工厂函数可以确保初始状态是有效且有意的。

  2. 强制执行业务规则(不变量): 工厂函数充当一个不可绕过的入口点,用于执行验证逻辑,从而保护类型的不变量。

  3. 封装复杂的初始化: 如果创建结构体需要注入依赖、初始化内部结构(如 map 或 channel),或执行非平凡的设置工作,工厂函数能将这些复杂性隐藏在调用者之外。

  4. 设计稳定的 API: 通过函数返回实例,可以将结构体的内部实现与客户端代码解耦。只要工厂函数的签名保持稳定,内部数据结构就能自由演进,而不会破坏已有代码。

  5. 管理依赖与可测试性: 工厂函数是 依赖注入(Dependency Injection) 的理想位置。它可以接收接口(抽象),返回具体的结构体。这种做法让单元测试更加容易,因为可以传入模拟实现(mock)。

代码示例:惯用工厂函数

以下示例展示了如何通过工厂函数进行验证,并提供清晰的失败路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
Name string
Age int
}

// 示例 1:带有验证和错误返回的工厂函数
func NewUser(name string, age int) (*User, error) {
if age < 18 {
return nil, errors.New("用户必须年满 18 岁")
}
return &User{
Name: name,
Age: age,
}, nil
}

函数选项模式(Functional Options Pattern, FOP)

函数选项模式(FOP) 是 Go 中广泛使用的一种惯用法,部分因一些有影响力的博客文章而流行开来。它的设计目标是:在对象初始化时,优雅地处理可能数量庞大或不断演化的可选参数。

定义函数选项模式

FOP 的核心思想是:通过向构造函数传入一系列可变参数(变长函数切片),每个“选项函数”接收一个指向结构体的指针,并修改该结构体的某个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"time"
)

type Server struct {
port string
timeout time.Duration
maxConnections int
}

// 1. 定义 Option 类型:本质上是修改 Server 结构体的函数
type ServerOptions func(*Server)

// 2. 定义 Option 构造器:返回具体的 Option 函数
func WithPort(port string) ServerOptions {
return func(s *Server) {
s.port = port
}
}

func WithTimeout(timeout time.Duration) ServerOptions {
return func(s *Server) {
s.timeout = timeout
}
}

func WithMaxConnections(maxConnections int) ServerOptions {
return func(s *Server) {
s.maxConnections = maxConnections
}
}

// 3. 工厂函数:应用所有传入的选项
func NewServer(options ...ServerOptions) *Server {
// 设置合理的默认值
server := &Server{
port: ":8080",
timeout: time.Second * 10,
maxConnections: 100,
}

// 应用自定义选项
for _, option := range options {
option(server)
}
return server
}

func main() {
myServer := NewServer(
WithMaxConnections(500),
WithTimeout(time.Second*30),
// WithPort 可选,默认值是 :8080
)
fmt.Printf("Server 配置: %+v\n", myServer)
}

FOP 的优势

  • 灵活性与兼容性: 对库的维护者来说,FOP 的最大优势在于他们可以轻松废弃旧选项、组合已有选项或新增选项,而不会破坏现有的客户端代码。

  • 封装性: 选项函数并不是 Server API 的一部分,它们不会为 Server 对象增加额外的方法或修改现有方法。

  • 构造保证: 调用工厂函数后,调用方始终能拿到一个 完整构造好的对象


FOP 的局限与解决方法

尽管 FOP 带来了很多好处,但在实际使用中也有一些缺点:

  • 命名污染: 选项函数会被暴露在包级命名空间下。如果你想为不同对象定义相同的功能(例如 WithTimeoutForServerWithTimeoutForClient),就可能导致命名混乱。
    👉 一种常见的解决方案是:使用空结构体类型来为选项分组,从而避免名称冲突。

  • 类型限制: Go 中函数只能有一组可变参数,并且类型必须相同。这意味着所有选项函数都必须共享同一函数签名,导致在需要多种类型参数时显得笨拙。

  • 可发现性差: 和显式的 Options 结构体不同,调用方可能更难直接看到有哪些可用选项。不过,很多人认为 IDE 的自动补全和 Go 的文档系统已经很好地缓解了这个问题。

  • 复杂度: 一些开发者认为这种模式过于复杂,本质上是一种“变相的补丁”,用来弥补 Go 缺少默认参数的缺陷。


一种常见的替代方案是:将选项定义为接口而非函数,这样可以提供更好的类型灵活性和可测试性。例如 Uber 的 zap 日志库中,选项定义如下:

1
2
3
type Option interface {
apply(*Logger)
}

这样,不同类型只要实现了 apply(*Logger) 方法,就都可以作为合法的 Option,避免了函数签名的限制。

与建造者模式的对比

函数选项模式(FOP)建造者模式(Builder Pattern) 都是为了解决一个问题:当对象有很多可选参数时,如何优雅地进行初始化。不过,两者采用的方式不同。


建造者模式(以及 Fluent Setters)

在 Go 中,人们有时所说的“建造者模式”其实更接近 Fluent Setters(流式 Setter 方法)方法链式调用
这种方式通常是定义一系列修改对象的 setter 方法,这些方法返回对象指针,以便可以链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 示例:Fluent Setters(常被误称为 Builder 模式)
type Server struct {
port string
timeout time.Duration
maxConnections int
}

func (s *Server) WithPort(port string) *Server {
s.port = port
return s
}

// ... 其他 With 方法 ...

func NewServer() *Server {
return &Server{}
}

func main() {
server := NewServer().
WithMaxConnections(500).
WithTimeout(time.Second * 30).
WithPort(":8080")
// fmt.Println(server)
}

对 Go 中 Fluent Setters 的批评:

这种 方法链式调用 在 Go 里通常被认为不太符合惯用风格,主要缺点是:

  • 它允许用户在对象创建 之后的任意时间 修改对象,这使得数据的状态难以推理,尤其是对一些期望在初始化完成后就保持不可变的服务类或服务器类来说。

  • 这种风格很难返回错误,也不利于编写可替换的 mock 对象。


真正的建造者模式

真正的建造者模式 会将“建造者对象”和“被构建对象”分离。
建造者对象收集所有的配置参数,最终对象只会在调用专门的 Build() 方法时才被创建,并且通常会返回一个错误值用于校验。
这种方式的优势在于:对象一旦构造完成,就不会再被随意修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 示例:真正的 Builder 模式
type server struct { // 非导出,强制只能通过 builder 构建
port string
timeout time.Duration
}

// 私有构造函数
func newServer(p string, t time.Duration) *server {
return &server{port: p, timeout: t}
}

type ServerBuilder struct {
Port string
Timeout time.Duration
// MaxConnections int // Builder 字段通常导出,方便字面量赋值
}

// Build 方法负责创建并返回最终配置好的 server
func (sb ServerBuilder) Build() (*server, error) {
if sb.Port == "" {
return nil, errors.New("必须指定端口")
}
// 应用配置,同时可执行验证
return newServer(sb.Port, sb.Timeout), nil
}

func main() {
// 使用字面量配置 Builder,然后必须调用 Build()
srv, err := ServerBuilder{
Port: ":8080",
Timeout: time.Second * 30,
}.Build()
// fmt.Println(srv, err)
}

FOP vs. Builder:该如何选择?

选择哪种模式,通常取决于具体的约束条件:

特性 函数选项模式(FOP) 建造者模式(True Builder)
初始化逻辑 更优: 构造函数控制选项的应用时机,可以在选项设置完成后立即执行必要的初始化逻辑,从而保证返回时对象处于有效状态。 需要单独的 Build() 方法;复杂的校验或必填配置可能需要在 Build() 内单独处理或强制执行。
必填参数 必填参数必须作为 New 函数的常规参数传入。 必填参数可以放在 Builder 的构造函数中,或者在 Build() 方法中强制检查。对于必填配置,Builder/配置结构体方案略有优势(有时被戏称为 “Dysfunctional Options Pattern”)。
易用性/人体工学 使用体验非常好,尤其是在调用方不需要传递很多选项、且没有必填参数的情况下。 可能显得冗长(样板代码较多),但在配置逻辑包含条件语句时(例如 if conf.SetBar { … })非常有用。
可扩展性 包的使用者可以很容易地实现自定义的选项函数。 对外部用户通常是封闭的;选项与 Builder 类型强绑定,不易扩展。
Go 惯用性 在 Go 生态中更常见,也被认为更“惯用”。 方法链式调用(fluent setters)通常被认为不太符合 Go 风格。真正的 Builder 模式更多用于需要顺序步骤的场景,例如 SQL 查询构建(db.Select().From().Where())。

在一些 从零设计 API 的场景下,如果调用通常不会涉及很多选项,或者存在必填参数,使用一个 简单的选项结构体 传入工厂函数,可能是更惯用的做法。
不过,FOP 依然是一个强有力的工具,能够优雅地管理可选配置,并为未来的扩展提供良好支持。

参考

  • Dave Cheney’s Post on Functional Options: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
  • Uber Go Style Guide on Functional Options: https://github.com/uber-go/guide/blob/master/style.md#functional-options
  • Dysfunctional Options Pattern in Go: https://rednafi.com/go/dysfunctional_options_pattern/
  • Tony Bai’s Go Constructor Pattern Guide: https://tonybai.com/2025/09/12/go-constructor-pattern-guide
  • Different ways to initialize Go structs (asankov.dev): https://asankov.dev/blog/2022/01/29/different-ways-to-initialize-go-structs/

更多内容

最近文章:

随机文章:


更多该系列文章,参考medium链接:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/go-function-option-patterns/
作者:微信公众号,Medium,LinkedIn,Twitter
发表日期:原文在 2025-09-21 16:02 时创作于 https://programmerscareer.com/zh-cn/go-function-option-patterns/
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

Go语言的变革:简单化、复杂化和稳定性 包管理器和依赖项的责任

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×