Functional Options

今天我来讲一下在go语言编程中一种很常用的编程模式 - Functional Options 模式。Functional Options 模式是目前在Go语言中最流行的一种编程模式, 在 Kubernetes 等开源项目中就有 Functional Options 的影子。接下来我们就来聊一聊 Functional Options 和它解决的问题。

常见的使用场景

在我们编程中,我们会经常性的需要对一个对象进行相关的配置。比如下面这个结构实体:

type Server struct {
    Addr     string
    Port     int
    Protocol string
    TLS      *tls.Config
}

在这个 Server 对象中,我们可以看到: 有侦听的IP地址 Addr 和端口号 Port ,这两个配置选项是必填的。 然后,还有协议 Protoco 字段,这几个字段是不能为空的,但是有默认值的,例如协议的默认值是tcp。 还有一个 TLS 这个是 HTTPS 安全链接,需要配置相关的证书和私钥。这个是可以为空的, 为空就是不使用 HTTPS。

所以根据上述结构体的描述,我们需要有多种不同的创建不同配置 Server 的函数签名,如下所示:

func NewDefaultServer(addr string, port int) (*Server, error) {
	return &Server{addr, port, "tcp", nil}, nil
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, tls *tls.Config) (*Server, error) {
	return &Server{addr, port, "tcp", tls}, nil
}

因为go语言与Python等语言是不同的,没用重载函数,所你得用不同的函数名来应对不同的配置选项。

新增配置对象

要解决这个问题,最常见的方式是使用一个配置对象,如下所示:


type Config struct {
    Protocol string
    TLS      *tls.Config
}
type Server struct {
    Addr string
    Port int
    Conf *Config
}

我们将那些不是必填的字段存入新的结构体中,于是Server结构体就变成如上图所示的样子。 于是,我们只需要一个 NewServer() 的函数了,在使用前需要构造 Config 对象。

func NewServer(addr string, port int, conf *Config) (*Server, error) {
   
}
conf := ServerConfig{Protocol:"tcp"}
srv, _ := NewServer("locahost", 9000, &conf)

这个代码要对 config 对象判断是否为 nil 。

使用 Functional Options 模式

接下来针对上面的场景,我们使用 Functional Options 模式进行编程。

首先我们先定义个函数类型:

type Option func(*Server)

然后我们根据需求定义如下一组函数

func Protocol(p string) Option {
    return func(s *Server) {
        s.Protocol = p
    }
}

func TLS(tls *tls.Config) Option {
    return func(s *Server) {
        s.TLS = tls
    }
}

上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的 Server 结构体的参数。例如:

当我们调用其中的一个函数用 Protocol(“tcp”) 时 其返回值是一个 func(s* Server) { s.Protocol = “tcp” } 的函数。 这个叫高阶函数在一些函数式编程语言中是经常使用的,可能不太好理解。

好了,现在我们再定一个 NewServer()的函数,其中,有一个可变参数 options 其可以传出多个上面上的函数,然后使用一个for-loop来设置我们的 Server 对象。

unc NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
  srv := Server{
    Addr:     addr,
    Port:     port,
    Protocol: "tcp",
    TLS:      nil,
  }
  for _, option := range options {
    option(&srv)
  }
  return &srv, nil
}

于是根据上面的定义,我们可以这样使用函数s1, _ := NewServer(“localhost”, 1024),这样就是使用 Functional Options 模式解决上面的问题。

Functional Options 模式带来了如下好处:

  • 直觉式的编程
  • 高度的可配置化
  • 很容易维护和扩展
  • 对于新来的人很容易上手

如下就是著名的 Kubernetes 项目中使用到的 Functional Options 模式 的一段代码。

// Option is a functional option type for Kubelet
type Option func(*Kubelet)
func WithRunAllFilters(runAllFilters bool) Option {
	return func(o *frameworkOptions) {
		o.runAllFilters = runAllFilters
	}
}

// WithPodNominator sets podNominator for the scheduling frameworkImpl.
func WithPodNominator(nominator framework.PodNominator) Option {
	return func(o *frameworkOptions) {
		o.podNominator = nominator
	}
}

// WithExtenders sets extenders for the scheduling frameworkImpl.
func WithExtenders(extenders []framework.Extender) Option {
	return func(o *frameworkOptions) {
		o.extenders = extenders
	}
}

以后在遇到类似的问题时,推荐大家使用 Functional Options 模式。