Golang - Option模式(2)(函数选项模式)

发布时间 2023-04-16 17:11:08作者: 李若盛开

函数式选项模式(Functional Options Pattern)

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

优缺点

选项模式有很多优点,例如:支持传递多个参数并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过 WithXXX 的函数命名,可以使参数意义更加明确,等等。 不过,为了实现选项模式,我们增加了很多代码。

适用场景:

  • 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
  • 结构体参数经常变动,变动时我们又不想修改创建实例的函数。例如:结构体新增一个 retry 参数,但是又不想在 NewConnect 入参列表中添加 retry int 这样的参数声明。 函数式选项模式一般应用在那些配置较多,且有可选参数的情况,如果结构体参数比较少,可以慎重考虑要不要采用选项模式。

举例

type Server struct{
  host string
  port int
  timeout time.Duration
  maxConn int
}

func New(host string, port int) *Server {
  return &Server{host, port}
}

func (s *Server) Start() error {
}

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

  • 为每个不同的配置选项声明一个新的构造函数
  • 使用专门的配置结构体来保存配置信息
  • 使用 Functional Option Pattern

方式一:新的构造函数

这种方式配置较少且不太会变化的情况。

func NewWithTimeout(host string, port int, timeout time.Duration) *Server {
  return &Server{host, port, timeout}
}

func NewWithTimeoutAndMaxConn(host string, port int, timeout time.Duration, maxConn int) *Server {
  return &Server{host, port, timeout, maxConn}
}

方式二:使用专门的配置结构体来保存配置信息

这种方式也是很常见的,特别是当配置选项很多时。即使将来增加更多配置选项,也可以轻松的完成扩展,不会破坏 Server 的 API,但当增加或删除选项,需要对 Config 有较大的修改。

type Server struct{
  cfg Config
}

// 通常可以创建一个 Config 结构体,其中包含 Server 的所有配置选项。
type Config struct {
  Host string
  Port int
  Timeout time.Duration
  MaxConn int
}

func New(cfg Config) *Server {
  return &Server{cfg}
}

方式三:使用 Functional Option Pattern

使用这种方式将来增加选项,只需要增加对应的 WithXXX 函数即可。 这种模式,在第三方库中使用挺多,比如 github.com/gocolly/colly

声明

// Option 类型是一个函数类型,它接收一个参数:*Server
type Option func(*Server)

// 定义一系列相关返回 Option 的函数:
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
  }
}

// Server 的构造函数接收一个 Option 类型的不定参数
func New(options ...Option) *Server {
  svr := &Server{}
  for _, option := range options {
    option(svr)
  }
  return svr
}

使用

package main

import (
  "log"
  "server"
)

func main() {
  svr := New(
    WithHost("localhost"),
    WithPort(8080),
    WithTimeout(time.Minute),
    WithMaxConn(120),
  )
  if err := svr.Start(); err != nil {
    log.Fatal(err)
  }
}

Uber 的 Go 语言编程规范中提到该模式时,建议定义一个 Option 接口,而不是 Option 函数类型。

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}
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    cache:  defaultCache,
    logger: zap.NewNop(),
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

已有项目改进

img.png img_1.png img_2.png

参考