Go工程化:函数式选项模式
go
go
字数统计: 1.6k(字)
阅读时长: 7(分)
引言
Go 不是完全面向对象语言,有一些面向对象模式不太适合它。但经过这些年的发展,Go 有自己的一些模式。今天介绍一个常见的模式:函数式选项模式(Functional Options Pattern)。
参考文献
引用官方博文:https://golang.cafe/blog/golang-functional-options-pattern.html
本文代码下载:https://github.com/mailjobblog/dev_go/tree/master/211223-FunOptionPattern
函数式选项模式介绍
Go 语言没有构造函数,一般通过定义 New 函数来充当构造函数。然而,如果结构有较多字段,要初始化这些字段,有很多种方式,但有一种方式认为是最好的,这就是函数式选项模式(Functional Options Pattern)。
函数式选项模式是一种在 Go 中构造结构体的模式,它通过设计一组非常有表现力和灵活的 API 来帮助配置和初始化结构体。
在 Uber 的 Go 语言规范中提到了该模式:
Functional options 是一种模式,在该模式中,你可以声明一个不透明的 Option 类型,该类型在某些内部结构中记录信息。你接受这些可变数量的选项,并根据内部结构上的选项记录的完整信息进行操作。
将此模式用于构造函数和其他公共 API 中的可选参数,你预计这些参数需要扩展,尤其是在这些函数上已经有三个或更多参数的情况下。
代码演示
为了更好的理解该模式,我们通过一个例子来讲解。定义一个 Server 结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main
type Server struct{ host string port int }
func New(host string, port int) *Server { return &Server{host, port} }
func (s *Server) Start() error { }
|
如何使用呢?
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main
import ( "log" "server" )
func main() { svr := New("localhost", 1234) if err := svr.Start(); err != nil { log.Fatal(err) } }
|
但如果要扩展 Server 的配置选项,如何做?通常有三种做法:
- 为每个不同的配置选项声明一个新的构造函数
- 定义一个新的 Config 结构体来保存配置信息
- 使用 Functional Option Pattern
下面,我们将对于这三种模式,进行详细的讲解和演示:
方法一
这种做法是在构造函数上,定义配置参数。有几个配置参数,则在 New 上就配置几个参数。用这种方式,当配置少的时候没有问题。但是,如果配置项比较多的时候将会是灾难。
这种方式的应用。比如 net 包中的 Dial 和 DialTimeout:
1 2
| func Dial(network, address string) (Conn, error) func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
|
完整演示代码示例:
Pkg.server
1 2 3 4 5 6 7 8 9 10 11 12 13
| type Server struct { host string port int }
func New(host string, port int) *Server { return &Server{host, port} }
func (s *Server) TestFunc() (string, error) { return fmt.Sprintf("Run Success, host:%s, port:%d \n", s.host, s.port), nil }
|
main
1 2 3 4 5
| func main() { s := server.New("127.0.0.1", 1234) test, err := s.TestFunc() fmt.Println(test, err) }
|
方法二
这种方式也是很常见的,特别是当配置选项很多时。通常可以创建一个 Config 结构体,其中包含 Server 的所有配置选项。这种做法,即使将来增加更多配置选项,也可以轻松的完成扩展,不会破坏 Server 的 API。
在使用时,需要先构造 Config 实例,对这个实例,又回到了前面 Server 的问题上,因为增加或删除选项,需要对 Config 有较大的修改。如果将 Config 中的字段改为私有,可能需要定义 Config 的构造函数。。。
完整演示代码示例:
Pkg.server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type Server struct { cfg Config }
type Config struct { Host string Port int }
func New(cfg Config) *Server { return &Server{cfg} }
func (s *Server) TestFunc() (string, error) { return fmt.Sprintf("Run Success, host:%s, port:%d \n", s.cfg.Host, s.cfg.Port), nil }
|
main
1 2 3 4 5 6 7 8
| func main() { s := server.New(server.Config{ Host: "127.0.0.1", Port: 1234, }) test, err := s.TestFunc() fmt.Println(test, err) }
|
方法三
一个更好的解决方案是使用 Functional Option Pattern。在这个模式中,我们定义一个 Option 函数类型:
1
| type Option func(*Server)
|
Option 类型是一个函数类型,它接收一个参数:*Server。然后,Server 的构造函数接收一个 Option 类型的不定参数:
1 2 3 4 5 6 7
| func New(options ...Option) *Server { svr := &Server{} for _, f := range options { f(svr) } return svr }
|
那选项如何起作用?需要定义一系列相关返回 Option 的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func WithHost(host string) Option { return func(s *Server) { s.host = host } }
func WithPort(port int) Option { return func(s *Server) { s.port = port } }
func WithTimeout(timeout time.Duration) Option { return func(s *Server) { s.timeout = timeout } }
func WithMaxConn(maxConn int) Option { return func(s *Server) { s.maxConn = maxConn } }
|
将来增加选项,只需要增加对应的 WithXXX 函数即可。
完整演示代码示例:
Pkg.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
| type Server struct { host string port int }
type Option func(*Server)
func New(options ...Option) *Server { ser := &Server{} for _, f := range options { f(ser) } return ser }
func WithHost(host string) Option { return func(s *Server) { s.host = host } }
func WithPort(port int) Option { return func(s *Server) { s.port = port } }
func (s *Server) TestFunc() (string, error) { return fmt.Sprintf("Run Success, host:%s, port:%d \n", s.host, s.port), nil }
|
main
1 2 3 4 5 6 7 8
| func main() { s := server.New( server.WithHost("127.0.0.1"), server.WithPort(1234), ) test, err := s.TestFunc() fmt.Println(test, err) }
|
扩展阅读
这种模式,在第三方库中使用挺多,比如 github.com/gocolly/colly:
1 2 3 4 5 6 7 8 9 10 11 12
| type Collector { } func NewCollector(options ...CollectorOption) *Collector
type CollectorOption{ } func AllowURLRevisit() CollectorOption func AllowedDomains(domains ...string) CollectorOption ...
|
不过 Uber 的 Go 语言编程规范中提到该模式时,建议定义一个 Option 接口,而不是 Option 函数类型。该 Option 接口有一个未导出的方法,然后通过一个未导出的 options 结构来记录各选项。
Uber 的这个例子能看懂吗?
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
| type options struct { cache bool logger *zap.Logger }
type Option interface { apply(*options) }
type cacheOption bool
func (c cacheOption) apply(opts *options) { opts.cache = bool(c) }
func WithCache(c bool) Option { return cacheOption(c) }
type loggerOption struct { Log *zap.Logger }
func (l loggerOption) apply(opts *options) { opts.logger = l.Log }
func WithLogger(log *zap.Logger) Option { return loggerOption{Log: log} }
func Open( addr string, opts ...Option, ) (*Connection, error) { options := options{ cache: defaultCache, logger: zap.NewNop(), }
for _, o := range opts { o.apply(&options) }
}
|