哈Ha!我向您介绍MárkSági-Kazár撰写的文章《类固醇的功能性选择》。
功能选项是Go的典范,适用于干净且可扩展的API。她受到Dave Cheney和Rob Pike的推崇。这篇文章是关于自模板首次引入以来的实践。
功能选项已成为一种通过包含可选参数的配置来创建良好且简洁的API的方法。有很多明显的方法可以做到这一点(构造函数,配置结构,setter等),但是当您需要传递许多选项时,它们的可读性很差,并且不能提供功能选项之类的优质API。
简介-什么是功能选项?
通常,当您创建“对象”时,会使用必要的参数调用构造函数:obj := New(arg1, arg2)
(让我们忽略以下事实:Go中没有传统的构造函数。)功能选项允许您使用其他参数扩展API,从而将上面的行变为以下内容:
obj := New(arg1, arg2)
obj := New(arg1, arg2, myOption1, myOption2)
功能选项基本上是可变函数的参数,该可变函数采用复合或中间配置类型作为参数。由于其可变的性质,即使不使用默认值,也可以不带任何选项地调用构造函数,保持其整洁,这是完全可以接受的。为了更好地演示该模式,让我们看一个实际的示例(不带功能选项):type Server struct {
addr string
}
func NewServer(addr string) *Server {
return &Server {
addr: addr,
}
}
添加超时选项后,代码如下所示:type Server struct {
addr string
timeout time.Duration
}
func Timeout(timeout time.Duration) func(*Server) {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
}
for _, opt := range opts {
opt(server)
}
return server
}
生成的API易于使用且易于阅读:
server := NewServer(":8080")
server := NewServer(":8080", Timeout(10 * time.Second))
server := NewServer(":8080", Timeout(10 * time.Second), TLS(&TLSConfig{}))
为了进行比较,下面是使用构造函数和config配置结构进行初始化的样子:
server := NewServer(":8080")
server := NewServerWithTimeout(":8080", 10 * time.Second)
server := NewServerWithTimeoutAndTLS(":8080", 10 * time.Second, &TLSConfig{})
server := NewServer(":8080", Config{})
server := NewServer(":8080", Config{ Timeout: 10 * time.Second })
server := NewServer(":8080", Config{ Timeout: 10 * time.Second, TLS: &TLSConfig{} })
使用功能选项而不是构造函数的好处可能很明显:它们更易于维护和读取/写入。当参数没有传递给构造函数时,功能参数也会超出配置结构。在以下各节中,我将显示更多示例,其中配置结构可能不符合预期。单击简介中的链接,以阅读功能选项的完整历史记录。(译者注-带有Dave Cheney原创文章的博客在俄罗斯并不总是可用,您可能需要通过VPN进行连接才能查看。此外,该文章中的信息也可以在他关于dotGo 2014的报告的视频中找到)功能选择实践
函数选项本身仅是传递给构造函数的函数。简单功能的易用性提供了灵活性,并具有很大的潜力。因此,多年来,模板周围出现了许多实践也就不足为奇了。下面,我将列出我认为最流行和有用的做法。如果您认为缺少某些内容,请给我发送电子邮件。选项类型
应用功能选项模板时,您要做的第一件事是确定可选功能的类型:
type Option func(s *Server)
尽管这看起来似乎不是一个很大的改进,但是通过使用类型名称而不是定义函数,代码变得更具可读性:func Timeout(timeout time.Duration) func(*Server) { }
func NewServer(addr string, opts ...func(s *Server)) *Server
func Timeout(timeout time.Duration) Option { }
func NewServer(addr string, opts ...Option) *Server
具有选项类型的另一个优点是Godoc将选项函数放在该类型下:
选项类型列表
通常,功能参数用于创建某些事物的单个实例,但并非总是如此。在创建多个实例时重用默认参数列表的情况也不鲜见:defaultOptions := []Option{Timeout(5 * time.Second)}
server1 := NewServer(":8080", append(defaultOptions, MaxConnections(10))...)
server2 := NewServer(":8080", append(defaultOptions, RateLimit(10, time.Minute))...)
server3 := NewServer(":8080", append(defaultOptions, Timeout(10 * time.Second))...)
这不是可读性很强的代码,尤其是考虑到使用功能选项的目的是创建友好的API。幸运的是,有一种方法可以简化此过程。我们只需要将[]选项直接切片即可:
func Options(opts ...Option) Option {
return func(s *Server) {
for _, opt := range opts {
opt(s)
}
}
}
用“选项”功能替换切片之后,上面的代码变为:defaultOptions := Options(Timeout(5 * time.Second))
server1 := NewServer(":8080", defaultOptions, MaxConnections(10))
server2 := NewServer(":8080", defaultOptions, RateLimit(10, time.Minute))
server3 := NewServer(":8080", defaultOptions, Timeout(10 * time.Second))
使用/设置选项的前缀
选项通常是复杂类型,与超时或最大连接数不同。例如,在服务器软件包中,您可以将Logger接口定义为一个选项(如果没有默认记录器,则可以使用它):type Logger interface {
Info(msg string)
Error(msg string)
}
显然,Logger名称不能用作选项名称,因为接口已经使用它了。您可以调用选项LoggerOption,但这不是一个非常友好的名称。如果您将构造函数视为一个句子,那么在我们的例子中,想到的是:WithLogger。func WithLogger(logger Logger) Option {
return func(s *Server) {
s.logger = logger
}
}
NewServer(":8080", WithLogger(logger))
复杂类型选项的另一个常见示例是值的切片:type Server struct {
whitelistIPs []string
}
func WithWhitelistedIP(ip string) Option {
return func(s *Server) {
s.whitelistIPs = append(s.whitelistIPs, ip)
}
}
NewServer(":8080", WithWhitelistedIP("10.0.0.0/8"), WithWhitelistedIP("172.16.0.0/12"))
在这种情况下,该选项通常将值附加到现有值上,而不是覆盖它们。如果需要覆盖一组现有值,则可以在选项名称中使用单词set:func SetWhitelistedIP(ip string) Option {
return func(s *Server) {
s.whitelistIPs = []string{ip}
}
}
NewServer(
":8080",
WithWhitelistedIP("10.0.0.0/8"),
WithWhitelistedIP("172.16.0.0/12"),
SetWhitelistedIP("192.168.0.0/16"),
)
预设模板
特殊用例通常足够通用以支持库中的使用。在配置的情况下,这可能意味着将一组参数分组在一起并用作预设使用。在我们的示例中,服务器可以具有开放式和内部使用方案,以不同的方式设置超时,速度限制,连接数等。
func PublicPreset() Option {
return Options(
WithTimeout(10 * time.Second),
MaxConnections(10),
)
}
func InternalPreset() Option {
return Options(
WithTimeout(20 * time.Second),
WithWhitelistedIP("10.0.0.0/8"),
)
}
尽管预设在某些情况下可能有用,但它们在内部库中可能具有很大的价值,而在公共库中,它们的价值则较小。默认类型值与默认预设值
Go始终具有默认值。对于数字,通常为零;对于布尔类型,则为false,依此类推。在可选配置中,依靠默认值被认为是一种好习惯。例如,空值应表示无限制的超时而不是不存在(通常是无意义的)。在某些情况下,默认类型值不是一个好的默认值。例如,Logger的默认值为nil,这可能会导致恐慌(如果您不通过条件检查来保护logger调用)。在这些情况下,在构造函数中设置值(在应用参数之前)是确定后备的好方法:func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
logger: noopLogger{},
}
for _, opt := range opts {
opt(server)
}
return server
}
我看到了带有默认预设的示例(使用上一节中描述的模板)。但是,我认为这不是一个好习惯。这与仅在构造函数中设置默认值相比,表达性要差得多:func NewServer(addr string, opts ...func(*Server)) *Server {
server := &Server {
addr: addr,
}
opts = append([]Option{DefaultPreset()}, opts...)
for _, opt := range opts {
opt(server)
}
return server
}
配置结构选项
将Config结构作为功能选项可能并不常见,但是有时会发现这种情况。这个想法是,功能选项是指配置结构,而不是指正在创建的实际对象:type Config struct {
Timeout time.Duration
}
type Option func(c *Config)
type Server struct {
config Config
}
当您有许多选项时,此模板很有用,并且创建配置结构似乎比在函数调用中列出所有选项更干净:config := Config{
Timeout: 10 * time.Second
}
NewServer(":8080", WithConfig(config))
此模板的另一个用例是设置默认值:config := Config{
Timeout: 10 * time.Second
}
NewServer(":8080", WithConfig(config), WithTimeout(20 * time.Second))
进阶范本
在编写了数十个功能选项之后,您可能会想知道是否有更好的方法可以做到这一点。不是在使用方面,而是在长期支持方面。例如,如果我们可以定义类型并将其用作参数该怎么办:type Timeout time.Duration
NewServer(":8080", Timeout(time.Minute))
(请注意,API的使用保持不变)事实证明,通过更改Option类型,我们可以轻松地做到这一点:
type Option interface {
apply(s *Server)
}
将可选功能重写为接口为实现功能选项的许多新方法打开了大门:各种内置类型可用作没有包装函数的参数:
type Timeout time.Duration
func (t Timeout) apply(s *Server) {
s.timeout = time.Duration(t)
}
选项和配置结构的列表(请参阅前面的部分)也可以重新定义如下:
type Options []Option
func (o Options) apply(s *Server) {
for _, opt := range o {
o.apply(s)
}
}
type Config struct {
Timeout time.Duration
}
func (c Config) apply(s *Server) {
s.config = c
}
我个人最喜欢的是能够在多个构造函数中重用该选项的功能:
type ServerOption interface {
applyServer(s *Server)
}
type ClientOption interface {
applyClient(c *Client)
}
type Option interface {
ServerOption
ClientOption
}
func WithLogger(logger Logger) Option {
return withLogger{logger}
}
type withLogger struct {
logger Logger
}
func (o withLogger) applyServer(s *Server) {
s.logger = o.logger
}
func (o withLogger) applyClient(c *Client) {
c.logger = o.logger
}
NewServer(":8080", WithLogger(logger))
NewClient("http://localhost:8080", WithLogger(logger))
摘要
功能参数是用于创建具有数十个参数的整洁且可扩展的API的强大模板。尽管与支持简单的配置结构相比,它所做的工作要多一些,但与替代方案相比,它提供了更大的灵活性并提供了更简洁的API。