golang中的函数/方法是不支持默认值和重载的,自己最近半年的工作封装了不少基础库,经常为此苦恼,但是由于工作量较大的原因,并未深入研究这个问题,最近google一下,其实dave cheney早在2014年就引入了详细的解决方案了。原文比较冗长,这里是文章核心内容和自己的理解。
个人将
functional options
翻译为函数化选项
提出问题
比如有以下函数:
func NewServer(addr string) (*Server, error) {
//...
}
Server可能有很多初始化参数/属性,比如支持tls,超时时间等等,怎么比较优雅的设置这些参数了?
解决方案1
通过将参数/属性抽象为一个Config,可以部分解决这个问题。 比如:
type ServerConfig struct {
Timeout time.Duration
//...
}
func NewServer(addr string, config *ServerConfig) (*Server, error) {
//默认初始化参数处理
if config != nil && config.Timeout == 0 {
config.Timeout = 默认参数
}
//...
}
config := &ServerConfig{
Timeout: 1*time.Second,
}
server, _ := NewServer(":9001", &config)
config.Timeout = 2*time.Second
自己主要使用的这个方案,但是这个方案还不够优雅,有两个问题:
-
- 初始化参数不太好处理,需要在NewServer中判断config是否为空且初始值是否有效,无效则设置默认值,这里的代码非常不友好,加重了心智负担。
-
- 上面代码的最后一行,server初始化后,再修改config的属性,会引起什么了?除了把Server的所有相关代码读一遍,否则无法预知。这里可以通过将NewServer的第二个参数类型的指针去掉,改为
func NewServer(addr string, config ServerConfig) (*Server, error)
- 上面代码的最后一行,server初始化后,再修改config的属性,会引起什么了?除了把Server的所有相关代码读一遍,否则无法预知。这里可以通过将NewServer的第二个参数类型的指针去掉,改为
在公司内部快速迭代开发过程中,个人觉得这种做法暂时是可以的,但是,非常不优雅,甚至可以说是问题较大,与go语言的尽量少的心智负担的理念相悖。需要尽早按照下面的第二种方案重构。
解决方案2
最佳实践应该是dave引入的下面这种模式:
func NewServer(addr string, options ...func(*Server)) (*Server, error) {
srv := Server{Timeout: 默认值}
// ...
}
func main() {
srv, _ := NewServer("localhost") //default
timeout := func(srv *Server) {
srv.timeout = 60 * time.Second
}
srv2, _ := NewServer("localhost", timeout)
}
这里的最佳实践可以让函数签名有以下特性,下面的翻译都是按照自己理解来的,还是看英文比较容易理解:
- 明确的默认值(sensible defaults)
- 高可配置(is hightly configurable)
- 今后易扩展(can grow over time)
- 自我描述(self documenting)
- 新手可控(safe for newcomers)
- 不需要nil或者空值对编译器友好(and never requires nil or an empty value to keep the compiler happy)
下面将dave自己重构的过程摘录一下,方便进一步理解: dave的老代码:
pakcage main
import "github.com/pkg/term"
func main() {
t, err := term.Open("/dev/ttyUSB0")
// handle error
err = t.SetSpeed(115200)
// handle error
err = t.SetRawMode()
// handle error
//...
}
使用functional options模式重构后:
pakcage main
import "github.com/pkg/term"
func main() {
// just open the terminal
t, _ := term.Open("/dev/ttyUSB0")
// open at 115200 baud in raw mode
t2, _ := term.Open("/dev/ttyUSB0", term.Speed(115200), term.RawMode)
}
在term包中,有如下代码:
func RawMode(t *Term) error { return t.setRawMode()}
func Speed(baud int) func(*Term) error {
return func(t *Term) error {
return t.setSpeed(baud)
}
}
func Open(dev string, options ...func(*Term) error) (*Term, error) {
t, err := openTerm(dev)
for _, option := range options {
err := option(t)
//handle err, cleanup
}
return t, nil
}
openTerm中可以设定默认的参数.
引申
掌握了这个模式,很多开源代码就可以看懂了,因为大部分写的比较好的开源库,基本都是用了这个模式,比如go-micro的NewService:
import (
"github.com/micro/go-micro"
// etcd v3 registry
"github.com/micro/go-plugins/registry/etcdv3"
// nats transport
"github.com/micro/go-plugins/transport/nats"
// kafka broker
"github.com/micro/go-plugins/broker/kafka"
)
func main() {
registry := etcdv3.NewRegistry()
broker := kafka.NewBroker()
transport := nats.NewTransport()
service := micro.NewService(
micro.Name("greeter"),
micro.Registry(registry),
micro.Broker(broker),
micro.Transport(transport),
)
service.Init()
service.Run()
}
反过来看,go-micro的代码质量是不错的,值得学习。
再比如自己经常使用的爬虫库gocolly,初始化爬虫也用了functional options:
func NewCollector(options ...func(*Collector)) *Collector {
c := &Collector{}
c.Init()
for _, f := range options {
f(c)
}
c.parseSettingsFromEnv()
return c
}