Golang 中应用 Functional Options 模式
Functional Options 是一个函数式编程的应用案例,编程技巧也很好,是目前在go语言中最流行的一种编程模式。在grpc的源码中有大量的应用。
要解决什么问题
配置选项问题
在编程中,我们会经常需要对一个对象进行相关的配置,比如:
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
在这个Server对象中,我们可以看到:
- 要有监听的ip地址
addr和端口号Port,这两个配置选项是必须的 - 还有
Protocol、Timeout、MaxConns字段,这几个字短是不能为空的,但有默认值:协议是tcp,超时时间30秒和最大连结束1024个 - 还有
TLS这个是安全链接,需要配置相关的证书和私钥。这个是可以为空的
所以,针对与上述这样的配置,因为go语言不支持重载函数,所以不得不用不同的函数来应对不同的配置创建Server的函数,如:
func NewDefaultServer(addr string, port int) (*Server, error) {
return &Server{
Addr: addr, Port: port,
Protocol: "tcp", Timeout: 30*time.Second, MaxConns: 1024
TLS: nil
}, nil
}
func NewTLSServer(addr string, port int, tlsConf *tls.Config) (*Server, error) {
return &Server{
Addr: addr, Port: port,
Protocol: "tcp", Timeout: 30*time.Second, MaxConns: 1024
TLS: tlsConf
}, nil
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
return &Server{
Addr: addr, Port: port,
Protocol: "tcp", Timeout: timeout, MaxConns: 1024,
TLS: nil
}, nil
}
如何解决
配置对象方案
要解决配置选项的问题,最常见的方式是使用一个配置对象,我们把哪些费必需的选项都移动到一个结构体里,于是Server对象变成了:
type Server struct {
Addr string
Port int
Conf *Config
}
于是我们只需要一个 NewServer() 的函数,使用前构造 Config 对象即可,如:
func NewServer(addr string, port int, conf *Config) (*Server, error) {
// ...
}
// using the default configuration
srv1, _ := NewServer("localhost", 9000, nil)
// with config
conf := Config{Protocol: "tcp", Timeout: 60 * time.Duration}
srv2, _ := NewServer("localhost", 9000, &conf)
这段代码算是很不错了,大多数情况下,我们可能就止步于此了。但是我们可以看到其中有一点不好的是:Config 并不是必须的,所以,需要在 NewServer 中判断是否是 nil 或 empty
Builder方案
如果熟悉设计模式的话,一定会很自然的使用上 builder 模式。采用这个模式,我们可以把代码改成如下的方式:
type ServerBuilder struct {
Server
}
func (s *ServerBuilder) Create(addr string, port int) *ServerBuilder {
s.Server.Addr = addr
s.Server.Port = port
return s
}
func (s *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
s.Server.Protocol = protocol
return s
}
// ...
func (s *ServerBuilder) WithTLS(tlsConf *tls.Config) *ServerBuilder {
s.Server.TLS = tlsConf
return s
}
func (s *ServerBuilder) Build() Server {
return s.Server
}
现在我们就可以用下面的方式使用:
sb := ServerBuilder{}
server := sb.Create("localhost", 8080).WithProtocol("udp").WithTimeout(30 * time.Second).Build()
上面这样的方式也很清楚,不需要额外的Config,使用链式的函数调用的方式来构造一个对象,只需要多加一个Builder。如果想省掉这个包装的结构体,那么就可以使用 Functional Options 来实现
Functional Options
首先,我们先定义一个函数类型,然后,我们可以使用函数式的方式定义一组函数:
type Option func(*Server)
func WithProtocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func WithMaxConns(maxConns int) Option {
return func(s *Server) {
s.MaxConns = maxConns
}
}
func WithTLS(tlsConf *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的 Server 函数,例如:
当我们调用其中一个函数使用
MaxConns(30)时,它的返回值是一个func(s *Server) { s.MaxConns = 30}的函数
这个叫做高阶函数,在数学上,就好像以下这样的数学定义:
- 计算长方形面积的公式为:
rect(w, h) = w * h - 这个函数我们包装一下,就可以变成计算正方形面积的公式:
square(w) = rect(w, w)
现在,我们在定义一个 NewServer 的函数,其中一个可变参数 options 可以传入上面的函数,然后使用 for-loop 来设置我们需要的 Server 对象:
func NewServer(addr string, port int, opts ...Option) (*Server, error) {
srv := &Server{
Addr: addr, Port: port, Protocol: "tcp", Timeout: 30 * time.Second, MaxConns: 1024,
TLS: nil,
}
for _, opt := range options {
opt(srv)
}
// ...
return srv, nil
}
于是,我们在创建 Server 的时候,我们就可以这样:
srv1, _ := NewServer("localhost", 9001)
srv2, _ := NewServer("localhost", 9001, WithProtocol("udp"))
srv3, _ := NewServer("localhost", 9001, WithTimeout(5 * time.Second), WithMaxConns(1024))
这样的方式,不但解决了使用 Config 方式时需要有一个config参数,但在不需要的时候,是放 nil 还是 Config{} 的选择困难,也不需要引用一个 Builder 的控制对象,直接使用函数式编程的方式,在代码阅读上也比较优雅。
Functional Options 方式至少可以带来以下好处:
- 直觉式的编程
- 高度的可配置化
- 很容易维护和扩展
- 自文档
- 对于团队新人来说很容易上手
- 没有令人困惑的事,如需要判断
nil或empty