Go编程模式:详解函数式选项模式

开发 后端
Go 不是完全面向对象语言,有一些面向对象模式不太适合它。但经过这些年的发展,Go 有自己的一些模式。今天介绍一个常见的模式:函数式选项模式(Functional Options Pattern)。

[[437104]]

大家好,我是 polarisxu。

Go 不是完全面向对象语言,有一些面向对象模式不太适合它。但经过这些年的发展,Go 有自己的一些模式。今天介绍一个常见的模式:函数式选项模式(Functional Options Pattern)。

01 什么是函数式选项模式

Go 语言没有构造函数,一般通过定义 New 函数来充当构造函数。然而,如果结构有较多字段,要初始化这些字段,有很多种方式,但有一种方式认为是最好的,这就是函数式选项模式(Functional Options Pattern)。

函数式选项模式是一种在 Go 中构造结构体的模式,它通过设计一组非常有表现力和灵活的 API 来帮助配置和初始化结构体。

在 Uber 的 Go 语言规范中提到了该模式:

Functional options 是一种模式,在该模式中,你可以声明一个不透明的 Option 类型,该类型在某些内部结构中记录信息。你接受这些可变数量的选项,并根据内部结构上的选项记录的完整信息进行操作。

将此模式用于构造函数和其他公共 API 中的可选参数,你预计这些参数需要扩展,尤其是在这些函数上已经有三个或更多参数的情况下。

02 一个示例

为了更好的理解该模式,我们通过一个例子来讲解。

定义一个 Server 结构体:

  1. package main 
  2.  
  3. type Server { 
  4.   host string 
  5.   port int 
  6.  
  7. func New(host string, port int) *Server { 
  8.   return &Server{host, port} 
  9.  
  10. func (s *Server) Start() error { 

如何使用呢?

  1. package main 
  2.  
  3. import ( 
  4.   "log" 
  5.   "server" 
  6.  
  7. func main() { 
  8.   svr := New("localhost", 1234) 
  9.   if err := svr.Start(); err != nil { 
  10.     log.Fatal(err) 
  11.   } 

但如果要扩展 Server 的配置选项,如何做?通常有三种做法:

为每个不同的配置选项声明一个新的构造函数

定义一个新的 Config 结构体来保存配置信息

使用 Functional Option Pattern

做法 1:为每个不同的配置选项声明一个新的构造函数

这种做法是为不同选项定义专有的构造函数。假如上面的 Server 增加了两个字段:

  1. type Server { 
  2.  
  3. host string 
  4.  
  5. port int 
  6.  
  7. timeout time.Duration 
  8.  
  9. maxConn int 
  10.  

一般来说,host 和 port 是必须的字段,而 timeout 和 maxConn 是可选的,所以,可以保留原来的构造函数,而这两个字段给默认值:

  1. func New(host string, port int) *Server { 
  2.  
  3. return &Server{host, port, time.Minute, 100} 
  4.  

然后针对 timeout 和 maxConn 额外提供两个构造函数:

  1. func NewWithTimeout(host string, port int, timeout time.Duration) *Server { 
  2.  
  3. return &Server{host, port, timeout} 
  4.  
  5.  
  6. func NewWithTimeoutAndMaxConn(host string, port int, timeout time.Duration, maxConn int) *Server { 
  7.  
  8. return &Server{host, port, timeout, maxConn} 
  9.  

这种方式配置较少且不太会变化的情况,否则每次你需要为新配置创建新的构造函数。在 Go 语言标准库中,有这种方式的应用。比如 net 包中的 Dial 和 DialTimeout:

  1. func Dial(network, address string) (Conn, error) 
  2.  
  3. func DialTimeout(network, address string, timeout time.Duration) (Conn, error) 

做法 2:使用专门的配置结构体

这种方式也是很常见的,特别是当配置选项很多时。通常可以创建一个 Config 结构体,其中包含 Server 的所有配置选项。这种做法,即使将来增加更多配置选项,也可以轻松的完成扩展,不会破坏 Server 的 API。

  1. type Server { 
  2.   cfg Config 
  3.  
  4. type Config struct { 
  5.   Host string 
  6.   Port int 
  7.   Timeout time.Duration 
  8.   MaxConn int 
  9.  
  10. func New(cfg Config) *Server { 
  11.   return &Server{cfg} 

在使用时,需要先构造 Config 实例,对这个实例,又回到了前面 Server 的问题上,因为增加或删除选项,需要对 Config 有较大的修改。如果将 Config 中的字段改为私有,可能需要定义 Config 的构造函数。。。

做法 3:使用 Functional Option Pattern

一个更好的解决方案是使用 Functional Option Pattern。

在这个模式中,我们定义一个 Option 函数类型:

  1. type Option func(*Server) 

Option 类型是一个函数类型,它接收一个参数:*Server。然后,Server 的构造函数接收一个 Option 类型的不定参数:

  1. func New(options ...Option) *Server { 
  2.  
  3. svr := &Server{} 
  4.  
  5. for _, f := range options { 
  6.  
  7. f(svr) 
  8.  
  9.  
  10. return svr 
  11.  

那选项如何起作用?需要定义一系列相关返回 Option 的函数:

  1. func WithHost(host string) Option { 
  2.  
  3. return func(s *Server) { 
  4.  
  5. s.host = host 
  6.  
  7.  
  8.  
  9. func WithPort(port intOption { 
  10.  
  11. return func(s *Server) { 
  12.  
  13. s.port = port 
  14.  
  15.  
  16.  
  17. func WithTimeout(timeout time.Duration) Option { 
  18.  
  19. return func(s *Server) { 
  20.  
  21. s.timeout = timeout 
  22.  
  23.  
  24.  
  25. func WithMaxConn(maxConn intOption { 
  26.  
  27. return func(s *Server) { 
  28.  
  29. s.maxConn = maxConn 
  30.  
  31.  

针对这种模式,客户端类似这么使用:

  1. package main 
  2.  
  3. import ( 
  4.  
  5. "log" 
  6.  
  7. "server" 
  8.  
  9.  
  10. func main() { 
  11.  
  12. svr := New( 
  13.  
  14. WithHost("localhost"), 
  15.  
  16. WithPort(8080), 
  17.  
  18. WithTimeout(time.Minute), 
  19.  
  20. WithMaxConn(120), 
  21.  
  22.  
  23. if err := svr.Start(); err != nil { 
  24.  
  25. log.Fatal(err) 
  26.  
  27.  

将来增加选项,只需要增加对应的 WithXXX 函数即可。

这种模式,在第三方库中使用挺多,比如 github.com/gocolly/colly:

  1. type Collector { 
  2.  
  3. // 省略... 
  4.  
  5.  
  6. func NewCollector(options ...CollectorOption) *Collector 
  7.  
  8. // 定义了一系列 CollectorOpiton 
  9.  
  10. type CollectorOption{ 
  11.  
  12. // 省略... 
  13.  
  14.  
  15. func AllowURLRevisit() CollectorOption 
  16.  
  17. func AllowedDomains(domains ...string) CollectorOption 
  18.  
  19. ... 

不过 Uber 的 Go 语言编程规范中提到该模式时,建议定义一个 Option 接口,而不是 Option 函数类型。该 Option 接口有一个未导出的方法,然后通过一个未导出的 options 结构来记录各选项。

Uber 的这个例子能看懂吗?

  1. type options struct { 
  2.   cache  bool 
  3.   logger *zap.Logger 
  4.  
  5. type Option interface { 
  6.   apply(*options) 
  7.  
  8. type cacheOption bool 
  9.  
  10. func (c cacheOption) apply(opts *options) { 
  11.   opts.cache = bool(c) 
  12.  
  13. func WithCache(c bool) Option { 
  14.   return cacheOption(c) 
  15.  
  16. type loggerOption struct { 
  17.   Log *zap.Logger 
  18.  
  19. func (l loggerOption) apply(opts *options) { 
  20.   opts.logger = l.Log 
  21.  
  22. func WithLogger(log *zap.Logger) Option { 
  23.   return loggerOption{Log: log} 
  24.  
  25. // Open creates a connection
  26. func Open
  27.   addr string, 
  28.   opts ...Option
  29. ) (*Connection, error) { 
  30.   options := options{ 
  31.     cache:  defaultCache, 
  32.     logger: zap.NewNop(), 
  33.   } 
  34.  
  35.   for _, o := range opts { 
  36.     o.apply(&options) 
  37.   } 
  38.  
  39.   // ... 

03 总结

在实际项目中,当你要处理的选项比较多,或者处理不同来源的选项(来自文件、来自环境变量等)时,可以考虑试试函数式选项模式。

注意,在实际工作中,我们不应该教条的应用上面的模式,就像 Uber 中的例子,Open 函数并非只接受一个 Option 不定参数,因为 addr 参数是必须的。因此,函数式选项模式更多应该应用在那些配置较多,且有可选参数的情况。

参考文献

https://golang.cafe/blog/golang-functional-options-pattern.html

https://github.com/uber-go/guide/blob/master/style.md#functional-options

责任编辑:武晓燕 来源: polarisxu
相关推荐

2022-11-06 23:17:23

Go语言项目

2010-07-15 17:58:31

Perl模式

2022-04-24 15:29:17

微服务go

2021-11-08 07:41:16

Go流水线编程

2023-05-04 08:47:31

命令模式抽象接口

2023-04-10 09:20:13

设计模式访客模式

2014-04-25 10:13:00

Go语言并发模式

2023-05-15 08:51:46

解释器模式定义

2022-02-21 08:15:15

Go项目语言

2021-07-12 10:24:36

Go装饰器代码

2012-06-15 11:27:55

ibmdw

2012-04-05 11:52:43

ibmdw

2012-08-30 14:12:49

IBMdW

2021-06-29 08:54:23

设计模式代理模式远程代理

2023-05-26 08:41:23

模式Go设计模式

2021-07-07 10:31:19

对象池模式解释器模式设计模式

2010-07-16 09:24:59

Perl模式

2023-03-27 00:20:48

2011-06-28 15:01:01

Qt PIMPL

2022-07-03 14:03:57

分布式Seata
点赞
收藏

51CTO技术栈公众号