真实世界的Go设计模式 - Builder设计模式

中文翻译成 建造者模式、生成器模式。

一个遵循《设计模式》一书臆造出来的例子如: Builder in Go / Design Patterns (refactoring.guru),复杂又难以理解。

在Go标准库中,一个常见的实现了Builder设计模式的例子是strings.Builderstrings.Builder类型提供了一种构建字符串的有效方式,特别是当您需要在循环中动态构建字符串时,这样可以避免不必要的内存分配和拷贝。

实现strings.Builder的关键点是使用了可变长度的缓冲区来存储字符串,并在构建过程中动态地增加其大小,以适应不断增长的字符串。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"strings"
)
func main() {
builder := strings.Builder{}
// 添加字符串片段到构建器
builder.WriteString("Hello, ")
builder.WriteString("World!")
// 获取构建好的字符串
result := builder.String()
fmt.Println(result) // 输出: Hello, World!
}

在这个例子中,我们首先使用strings.Builder{}创建了一个新的构建器。然后,通过调用WriteString()方法,我们可以向构建器添加字符串片段。最后,通过调用String()方法,我们可以获得构建好的最终字符串。

这里值得注意的是,strings.Builder在内部使用了一个可变长度的[]byte缓冲区,它会根据需要自动增长。这样,在构建字符串时,strings.Builder会根据当前缓冲区的大小和新添加的字符串片段的长度来决定是否需要扩展缓冲区的大小,从而避免了频繁的内存分配和拷贝操作。

这种实现方式在构建大型字符串时非常高效,因为它最小化了内存分配和拷贝的开销,同时还能提供简洁而灵活的API来构建字符串。这是Go标准库中使用Builder设计模式的一个很好的例子。

在地道的Go语言中,很少看到真正明显的使用Builder进行构建对象的例子。第一,Go一般直接New一个对象,传入相应的值进行字段的设置,相当的简洁粗暴,第二,Go的风格是很少使用方法链式调用(Method Chaining),而是使用功能选项风格(functional option)。链式调用是很好实现Builder的一种方式。

比如下面这个例子,我们对*http.Request进行包装,生成一个新的支持链式构建的Request。在创建一个新的Request的时候,我们使用链式调用进行初始化,增加了Cookie和查询参数:

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
package chaining
import (
"fmt"
"net/http"
)
type Request struct {
*http.Request
}
func main() {
// 使用链式调用创建一个GET请求
request, _ := http.NewRequest("GET", "https://www.example.com", nil)
req := &Request{request}
req.WithHeader("User-Agent", "MyCustomUserAgent").
WithCookie(&http.Cookie{Name: "session", Value: "abc123"}).
WithQueryParam("key", "value")
// 打印构建好的请求
fmt.Println(request)
}
// WithHeader 添加请求头
func (r *Request) WithHeader(key, value string) *Request {
r.Header.Add(key, value)
return r
}
// WithCookie 添加Cookie
func (r *Request) WithCookie(cookie *http.Cookie) *Request {
r.AddCookie(cookie)
return r
}
// WithQueryParam 添加URL查询参数
func (r *Request) WithQueryParam(key, value string) *Request {
query := r.URL.Query()
query.Add(key, value)
r.URL.RawQuery = query.Encode()
return r
}

但是,这种方式在Go生态圈中还是比较少用的,比如*http.Request还是通过逐步调用进行一步步的构建。

现在在Go生态圈流行一种叫functional option的创建模式。这个想法最早来自于Rob Pike的Self-referential functions and the design of options),后来不知道怎么被大佬们介绍就流行起来了。Go一般创建对象会使用以下几个方式:

  • New(x int, y stirng, z S):选项太多时参数太长。

  • NewXXX(...)、NewYYY(...)、NewZZZ(...):为每个配置选项声明一个新的构造函数,多个选项同时支持时方法太多。

  • New(config Config):定义一个配置对象,这是在很多选项的情况很多gopher采用的一种方式

  • New(options ...func(*Server)): 使用功能选项WithHost、WithPort、WithTimeout等作为opton

  • 链式构建:如上面的例子,Go生态圈中少用

我看到功能选项有被滥用的趋势,也有人建议我开发的rpcx框架中使用功能选项模式,我拒绝了,因为简单的一个字段的赋值就使用功能选项,我个人认为有点大材小用了,但是我也没有大张旗鼓的去写文章去批判它,我怕会被它的拥趸群起攻之。选择自己喜欢的就好。所以etcd的作者就同时使用了两种方式,萝卜白菜,各取所爱:

1
2
func New(cfg Config) (*Client, error)
func NewCtxClient(ctx context.Context, opts ...Option) *Client

写于 7.26凌晨