大家好,我是polarisxu。Go不是完全面向对象的语言,一些面向对象的模式并不适合它。但是经过多年的发展,围棋有了自己的一些模式。今天我们介绍一种常见的模式:FunctionalOptionsPattern。01什么是函数选项模式Go语言没有构造函数,New函数一般定义为构造函数。但是,如果结构体的字段比较多,初始化这些字段的方式有很多种,但目前认为最好的一种方式,就是FunctionalOptionsPattern。FunctionalOptions模式是Go中的一种结构化模式,它通过设计一个非常有表现力和灵活的API来帮助配置和初始化结构。Uber的Go语言规范中提到了该模式:Functionaloptions是一种模式,您可以在其中声明一个不透明的Option类型,该类型在某些内部结构中记录信息。您接受这些可变数量的选项,并根据内部结构上选项记录的完整信息进行操作。将此模式用于构造函数和其他您希望扩展的公共API中的可选参数,尤其是在这些函数上已经有三个或更多参数的情况下。02一个例子为了更好的理解这个模式,我们通过一个例子来解释它。定义一个Server结构体:packagemaintypeServer{hoststringportint}funcNew(hoststring,portint)*Server{return&Server{host,port}}func(s*Server)Start()error{}如何使用?packagemainimport("log""server")funcmain(){svr:=New("localhost",1234)iferr:=svr.Start();err!=nil{log.Fatal(err)}}但是如果要扩展Server的配置选项,怎么做?通常有三种方法:为每个不同的配置选项声明一个新的构造函数定义一个新的Config结构来保存配置信息使用功能选项模式方法1:为每个不同的配置选项声明一个新的构造函数这一种方法是定义专有的不同选项的构造函数。如果上面的Server增加了两个字段:typeServer{hoststringportinttimeouttime.DurationmaxConnint}一般来说,host和port是必填字段,而timeout和maxConn是可选的,所以可以保留原来的构造函数,这两个字段给出默认值:funcNew(hoststring,portint)*Server{return&Server{host,port,time.Minute,100}}然后为timeout和maxConn提供两个额外的构造函数:funcNewWithTimeout(hoststring,portint,timeouttime.Duration)*Server{return&Server{host,port,timeout}}funcNewWithTimeoutAndMaxConn(hoststring,portint,timeouttime.Duration,maxConnint)*Server{return&Server{host,port,timeout,maxConn}}这个方法配置少,不太可能改,不然每次都需要新建构造函数用于新配置。在Go语言标准库中,就有这种方法的应用。比如net包中的Dial和DialTimeout:funcDial(network,addressstring)(Conn,error)funcDialTimeout(network,addressstring,timeouttime.Duration)(Conn,error)做法二:使用特殊的配置结构也很常见,特别是当有很多配置选项时。通常您可以创建一个Config结构,其中包含服务器的所有配置选项。这样,即使以后增加更多的配置选项,也可以在不破坏ServerAPI的情况下轻松扩展。使用typeServer{cfgConfig}typeConfigstruct{HoststringPortintTimeouttime.DurationMaxConnint}funcNew(cfgConfig)*Server{return&Server{cfg}}时,需要先构造一个Config实例。对于这个实例,又回到了之前Server的问题,因为增加或删除options,需要大量修改Config。如果将Config中的字段设为私有,则可能需要定义Config的构造函数。..实践3:使用功能选项模式更好的解决方案是使用功能选项模式。在这个模式中,我们定义了一个Option函数类型:typeOptionfunc(*Server)Option类型是一种带有一个参数的函数类型:*Server。然后,Server的构造函数接收到一个类型为Option的不确定参数:funcNew(options...Option)*Server{svr:=&Server{}for_,f:=rangeoptions{f(svr)}returnsvr}该选项如何工作??需要定义一系列返回Option的函数:funcWithHost(hoststring)Option{returnfunc(s*Server){s.host=host}}funcWithPort(portint)Option{returnfunc(s*Server){s.port=port}}funcWithTimeout(timeouttime.Duration)Option{returnfunc(s*Server){s.timeout=timeout}}funcWithMaxConn(maxConnint)Option{returnfunc(s*Server){s.maxConn=maxConn}}对于这种模式,客户端像这样使用:packagemainimport("log""server")funcmain(){svr:=New(WithHost("localhost"),WithPort(8080),WithTimeout(time.Minute),WithMaxConn(120),)iferr:=svr.Start();err!=nil{log.Fatal(err)}}以后添加选项,添加对应的WithXXX函数即可。这种模式在第三方库中使用较多,比如github.com/gocolly/colly:typeCollector{//省略...}funcNewCollector(options...CollectorOption)*Collector//定义了一系列CollectorOpitontypeCollectorOption{//Omit...}funcAllowURLRevisit()CollectorOptionfuncAllowedDomains(domains...string)CollectorOption...但是Uber的Go语言编程规范提到这种模式时,建议定义一个Option接口,而不是Option函数类型。Option接口有一个unexported方法,然后通过一个unexportedoptions结构记录每一个选项。你能理解优步的这个例子吗?typeoptionsstruct{cacheboollogger*zap.Logger}typeOptioninterface{apply(*options)}typecacheOptionboolfunc(ccacheOption)apply(opts*options){opts.cache=bool(c)}funcWithCache(cbool)Option{returncacheOption(c)}typeloggerOptionstruct{Log*zap.Logger}func(lloggerOption)apply(opts*options){opts.logger=l.Log}funcWithLogger(log*zap.Logger)Option{returnloggerOption{Log:log}}//Opencreatesaconnection.funcOpen(addrstring,opts)...Option,)(*Connection,error){options:=options{cache:defaultCache,logger:zap.NewNop(),}for_,o:=rangeopts{o.apply(&options)}//...}03小结在实际项目中,当你需要处理的选项比较多,或者来自不同来源(来自文件,来自环境变量等)的选项时,可以考虑尝试函数式选项模型。注意,在实际工作中,我们不要教条地套用上述模式,就像Uber中的例子,Open函数不只接受一个Option变量参数,因为addr参数是必须的。因此,在配置和可选参数较多的情况下,应该更多地应用功能选项模式。参考资料https://golang.cafe/blog/golang-functional-options-pattern.htmlhttps://github.com/uber-go/guide/blob/master/style.md#functional-options
