Contents

Go编程模式:Functional Options

函数选项问题

在日常开发中,我们经常遇到需要将业务实体初始化的场景,此时必然会定义一些可变参数,如下图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("..."))
...

在我看来,这种实现有三种好处:

  1. 使用了go的变长参数,函数签名不会因后续功能拓展而改变。
  2. 支持默认值,在上面例子可以看到,为实例配置默认值十分方便简洁。
  3. 调用的代码十分直观。

缺点也不是没有,代码相对冗长,且实现方式不是很直观,需要时间理解。