函数选项问题
在日常开发中,我们经常遇到需要将业务实体初始化的场景,此时必然会定义一些可变参数,如下图Server结构体这个例子:
1
2
3
4
5
6
7
|
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
TLS *tls.Config
}
|
其中,只有Addr和Port是必须的,那么我们应该如何去实现初始化Server
对象的函数呢?
原始做法
实现所有可选参数的组合的函数:
1
2
3
4
|
func NewServer(Addr string, Port int) (*Server, error) {...}
func NewServerProtocol(Addr string, Port int, Protocol string) (*Server, error) {...}
func NewServerProtocolAndTimeout(Addr string, Port int, Protocol string, Timeout time.Duration) (*Server, error) {...}
...
|
这明显不是一个可行的做法。
参数对象做法
比较常见的做法是定义一个ServerConfig
对象,里面承载所有非必须的选项:
1
2
3
4
5
|
type ServerConfig struct {
Protocol string
Timeout time.Duration
TLS *tls.Config
}
|
然后在初始化函数里作为参数传入:
1
2
3
4
|
func NewServer(Addr string, Port int, Options *ServerConfig) (*Server, error) {...}
//use case:
conf := ServerConfig{Protocol : "tcp", Timeout: 60*time.Second}
srv, _ := NewServer("localhost", 443, &conf)
|
这样做相对优雅不少,其实我个人蛮喜欢这种模式的:它把对象的配置显式地暴露在函数外面,让使用者清楚知道有那些可选项。但缺点是对于调用者未使用的值,假如初始化时想要给它赋予默认值,则需要一个个判断是否有为nil或者空才能覆盖,比较麻烦。
函数选项模式
函数选项模式(Functional Options)是Rob Pike在2014年提出的一种实例初始化模式,它很好的解决上面提到的各问题。
具体实现如下,我们首先定义一个函数类型:
1
|
type Option func(*Server)
|
然后为每一个Server
的可选参数定义一个Option
类型函数的builder,它返回用于修改Server
内对应参数的函数:
1
2
3
4
5
6
7
8
9
10
11
12
|
func WithProtocol(proto string) Option {
return func(s *Server) {
s.Protocol = proto
}
}
func WithTimeout(timeout time.Second) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
...
|
然后我们在初始化时,接受Option
类型函数作为我们的可选参数,并在初始化时,逐一调用这些函数,传入当前的实例,让Option
函数去进行参数的修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func NewServer(addr string, port int, options ...Option) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
TLS: nil
}
for _, option in options {
option(&srv)
}
return &srv, nil
}
|
当我们需要创建实例时,就可以灵活地传入可选参数了:
1
2
3
|
srv, _ := NewServer("localhost", 443, WithTimeout(20*time.Second))
srv, _ := NewServer("localhost", 443, WithProtocol("udp"), WithTLS("..."))
...
|
在我看来,这种实现有三种好处:
- 使用了go的变长参数,函数签名不会因后续功能拓展而改变。
- 支持默认值,在上面例子可以看到,为实例配置默认值十分方便简洁。
- 调用的代码十分直观。
缺点也不是没有,代码相对冗长,且实现方式不是很直观,需要时间理解。