Go 语言中的可选参数与创建型模式。这篇文章摘录自耗子哥博客-Go编程模式
1. Function Options
Functional Options 编程模式是一个函数式编程的应用案例,与传统的 Builder 模式有关,编程技巧也很好,是目前在Go语言中最流行的一种编程模式。不多在讨论这个模式之前,我们先来看看要解决什么样的问题。
1.1 配置选项问题
编程中,我们经常需要对一个对象进行相关配置,比如:
1
2
3
4
5
6
7
8
|
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
|
在这个 Server 对象中
- IP地址 Addr 和端口号 Port 是必填的(假设)
- 协议 Protocol 、 Timeout 和MaxConns 字段,不能为空,但是有默认值
- TLS 需要配置证书和密钥,可以为空
所以,针对于上述这样的配置,我们需要有多种不同的创建不同配置 Server 的函数签名,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func NewDefaultServer(addr string, port int) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
return &Server{addr, port, "tcp", timeout, 100, nil}, nil
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}
|
因为Go语言不支持重载函数,所以,你得用不同的函数名来应对不同的配置选项。
这个问题按照简介程度有不同的解决方案:
- 使用一个单独的配置对象
- Builder 构建者模式
- Functional Options
下面我们一一来介绍。
1.2 配置对象方案
配置对象方案是将所有的非必须选项移动到一个独立的配置对象中:
1
2
3
4
5
6
7
8
9
10
11
12
|
type Config struct {
Protocol string
Timeout time.Duration
Maxconns int
TLS *tls.Config
}
type Server struct {
Addr string
Port int
Conf *Config
}
|
于是我们只需要一个 NewServer()
构造函数,但在使用前需要构建 Config 对象。
1
2
3
4
5
6
7
8
9
|
func NewServer(addr string, port int, conf *Config) (*Server, error) {
//...
}
//Using the default configuratrion
srv1, _ := NewServer("localhost", 9000, nil)
conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &conf)
|
但是这个方案有这么一些缺点:
- Config 并不是必需的
- 代码内需要判断 conf 是否为 nil 或者 Empty-Config{},代码不是非常简洁
1.3 Builder 模式
如果熟悉设计模式,我们很容易想到 Builder 模式:
1
2
3
4
5
|
User user = new User.Builder()
.name("Hao Chen")
.email("haoel@hotmail.com")
.nickname("左耳朵")
.build();
|
仿照上面,我们可以把 Server 的创建改写成这样(这里面忽略了异常处理的部分):
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
|
//使用一个builder类来做包装
type ServerBuilder struct {
Server
}
func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
sb.Server.Addr = addr
sb.Server.Port = port
//其它代码设置其它成员的默认值
return sb
}
func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
sb.Server.Protocol = protocol
return sb
}
func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
sb.Server.MaxConns = maxconn
return sb
}
func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
sb.Server.Timeout = timeout
return sb
}
func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
sb.Server.TLS = tls
return sb
}
func (sb *ServerBuilder) Build() (Server) {
return sb.Server
}
|
于是我们就可以按照如下方式来使用了:
1
2
3
4
5
6
|
sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
WithProtocol("udp").
WithMaxConn(1024).
WithTimeOut(30*time.Second).
Build()
|
上面这个代码结构优点是:
- 上面这样的方式也很清楚,不需要额外的Config类
- 使用链式的函数调用的方式来构造一个对象,只需要多加一个Builder类
但是似乎:
- 这个Builder类似乎有点多余,我们似乎可以直接在Server 上进行这样的 Builder 构造
- 在处理错误的时候可能就有点麻烦(需要为Server结构增加一个error 成员,破坏了Server结构体的“纯洁”),不如一个包装类更好一些
如果我们想省掉这个包装的结构体,那么就轮到我们的Functional Options上场了,函数式编程。
1.3 Functional Options
首先,我们先定义一个函数类型:type Option func(*Server)
然后,我们可以使用函数式的方式定义一组如下的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
|
上面这些函数接收一个参数,返回一个可以配置 Server 的另一个函数。例如:
- 当我们调用其中的一个函数用
MaxConns(30)
时
- 其返回值是一个
func(s* Server) { s.MaxConns = 30 }
的函数。
好了,现在我们再定一个 NewServer()的函数,其中,有一个可变参数 options 其可以传入多个上面的返回值函数,然后使用一个for-loop来设置我们的 Server 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
|
于是,我们在创建 Server 对象的时候,我们就可以这样来了。
1
2
3
|
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
|
高度舒适:
- 不但解决了使用 Config 对象方式 的需要有一个config参数,但在不需要的时候,是放 nil 还是放 Config{}的选择困难
- 也不需要引用一个Builder的控制对象
- 直接使用函数式编程,在代码阅读上也很优雅
1.4 个人体会
不同的编程语言,因为语法上的差异,在设计模式或者说特定功能的实现上还是存在着一些明显的差异。所以要想写出特定语言的专业代码,去研究特定语言的23种设计模式实现还是很有必要的。随着软件编程的不断进步,更新的更优雅的编程模式也在不断出现,需要我们与时俱进。
但是想保持与时俱进并不容易,就拿 23 中设计模式来说,网上有关 Go 语言的实现,很多都是简单的复刻,而不是基于 Go 的最佳实践。作为互联网人,有效的获取我们需要的知识其实一个非常重要的能力。
2. grpc 中的配置处理
Functional Options 在众多的 go package 中都被使用。我们就以 grpc 中的 ServerOption 作为示例,简单介绍一下他的使用:
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
type ServerOption interface {
apply(*serverOptions)
}
// 1. 服务端所有的配置选项
type serverOptions struct {
creds credentials.TransportCredentials
codec baseCodec
cp Compressor
dc Decompressor
unaryInt UnaryServerInterceptor
streamInt StreamServerInterceptor
chainUnaryInts []UnaryServerInterceptor
chainStreamInts []StreamServerInterceptor
...
}
// 2. 默认的参数配置
var defaultServerOptions = serverOptions{
maxReceiveMessageSize: defaultServerMaxReceiveMessageSize,
maxSendMessageSize: defaultServerMaxSendMessageSize,
connectionTimeout: 120 * time.Second,
writeBufferSize: defaultWriteBufSize,
readBufferSize: defaultReadBufSize,
}
// 3. Functional Options,不过这里多了一层包装
// funcServerOption wraps a function that modifies serverOptions into an
// implementation of the ServerOption interface.
type funcServerOption struct {
f func(*serverOptions)
}
func (fdo *funcServerOption) apply(do *serverOptions) {
fdo.f(do)
}
// f func(*serverOptions) 就是 Functional Options,不过 grpc 多了一层包装
func newFuncServerOption(f func(*serverOptions)) *funcServerOption {
return &funcServerOption{
f: f,
}
}
// 4. Function Options 配置
func WriteBufferSize(s int) ServerOption {
return newFuncServerOption(func(o *serverOptions) {
o.writeBufferSize = s
})
}
// 5. 服务初始化
func NewServer(opt ...ServerOption) *Server {
// 默认参数
opts := defaultServerOptions
// funcServerOption.apply(&opts)
// f(&opts)
for _, o := range opt {
o.apply(&opts)
}
s := &Server{
lis: make(map[net.Listener]bool),
opts: opts,
conns: make(map[string]map[transport.ServerTransport]bool),
services: make(map[string]*serviceInfo),
quit: grpcsync.NewEvent(),
done: grpcsync.NewEvent(),
czData: new(channelzData),
}
// 处理拦截器的链式调用
// chainUnaryServerInterceptors chains all unary server interceptors into one.
chainUnaryServerInterceptors(s)
chainStreamServerInterceptors(s)
s.cv = sync.NewCond(&s.mu)
if EnableTracing {
_, file, line, _ := runtime.Caller(1)
s.events = trace.NewEventLog("grpc.Server", fmt.Sprintf("%s:%d", file, line))
}
if s.opts.numServerWorkers > 0 {
s.initServerWorkers()
}
s.channelzID = channelz.RegisterServer(&channelzServer{s}, "")
channelz.Info(logger, s.channelzID, "Server created")
return s
}
|