跳转至

可接受不定项命名参数的函数

问题

需要对一个对象或者业务实体进行相关的配置,比如:

Go
type Server struct{
    Addr string
    Port int
    Protocol string
    Timeout time.Duration
    ...
}
因为Go不支持重载函数,除了使用像下面这样的不同函数签名来应对不同的配置选项,还有什么简便直观的方法。
Go
func NewDefaultServer(addr string,port int)*Server{
    return &Server{addr,port,"tcp",60*time.Second,...}
}

func NewUDPServer(addr string,port int)*Server{
    return &Server{addr,port,"udp",60*time.Second,...}
}

func NewServerWithTimeout(addr string,port int,timeout time.Duration)*Server{
    return &Server{addr,port,"udp",timeout,...}
}
...

解决方案

Struct Config方法

设置一个配置对象

Go
type Config struct{
    Timeout time.Duration
    Protocol string
}

于是我们的NewServer()函数变成了

Go
func NewServer(addr string,port int,conf *Config)*Server{
    srv := Server{addr,port} //缺省
    srv.Timeout =conf.Timeout
    srv.Protocol = conf.Protocol
    return &srv
}

调用如下:

Go
src := NewServer("localhost",8080,&Config{"udp",60*time.Second})
如果额外的参数为空,需要置nil
Go
src := NewServer("localhost",8080,nil)

Builder模式/Chain-With模式

对于第一种Struct Config方法,有个不太好的地方就是Config不是必须的,需要判断nil或空Config{},这会让代码看起来不太干净。如果你熟悉设计模式,一定会想到Builder模式。

Go
type ServerBuilder struct{
    Server{}
}

func (sb *ServerBuilder)Create(addr string,port int)*ServerBuilder{
    sb.Server.Addr = addr
    sb.Server.Port = port
    return sb
}

func (sb *ServerBuilder)WithTimeout(timeout time.Duration)(*ServerBuilder){
    sb.Server.Timeout = timeout
    return sb
}

func (sb *ServerBuilder)WithProtocol(protocol string)(*ServerBuilder){
    sb.Server.Protocol = protocol
    return sb
}
...
func (sb *ServerBuilder)Build()(Server){
    return sb.Server
}

于是就可以用下面的方式调用了:

Go
server:= ServerBuilder{}.Create("localhost",8080).
                         WithTimeout(60*time.Second).
                         WithProtocol("tcp").
                         Build()

上面这样的方式不需要额外的Config类,使用链式调用的方式来构建一个对象。

其中ServerBuilder也不是必要的,可以在Server中直接添加Build方法。增加ServerBuilder这样一个包装类的优势是:

  • 便于处理error异常
  • 保持Server的纯洁干净,无额外的辅助信息和方法

函数式选项

如果真的想省略ServerBuilder这个包装类,可以使用函数式选项的方法。

首先,先定义一个函数类型

Go
type Option func(*Server)

然后,我们可以使用函数式的方式定义一组如下的函数:

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

func Timeout(t timeout.Duration)Option{
    return func(s *Server){
        s.Timeout = t
    }
}
上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的Server参数。

例如:

当我们调用其中一个函数/方法Protocol("tcp")时,其返回值是一个func(s *Server){s.Protocol="tcp"}的函数。

这个叫高阶函数。

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

Go
func NewServer(addr string,port int,options ...Option)(*Server,error){
    srv:= Server{
        Addr:addr,
        Port:port,
    }
    for _,option := range options{
        option(&srv)
    }
    //...
    retrun &srv,nil

}

于是创建Server对象的时候就可以:

Go
srv1,_ := NewServer("localhost",8080)
srv2,_ := NewServer("localhost",8080,Protocol("tcp"))
srv3,_ := NewServer("localhost",8080,Timeout(60*time.Second))
srv4,_ := NewServer("localhost",8080,Protocol("tcp"),Timeout(60*time.Second))

不需要考虑Config对象的nil,也不用Builder对象,是不是更整洁优雅且语义清楚,像极了Python中的不定命名参数?

在编写正式API的时候,推荐使用这种方式,其至少有下面这些优点:

  • 直觉编程
  • 高度可配置
  • 很容易维护和扩展
  • 自文档
  • 优雅好看易懂

评论