熟悉Python开发的同学都知道,Python有默认参数,这样我们在实例化一个对象的时候,可以根据需要选择性地覆盖一些默认参数,从而决定如何实例化对象目的。当一个对象有多个默认参数并优雅地简化代码时,此功能会派上用场。Go语言在语法上不支持默认参数,所以要通过默认参数创建对象和传递自定义参数,需要一些编程技巧来实现。针对程序开发中的这些常见问题,软件行业的先驱们总结出了许多解决常见场景编码问题的最佳实践,而这些最佳实践后来也成为了我们所说的设计模式。其中,选项模式在Go语言开发中经常使用。通常我们通过默认参数创建对象和通过自定义参数创建对象有以下三种方式:使用多个构造函数默认参数optionoptionmodethroughmultipleconstructors第一种方式是通过多个构造函数来实现,这里举一个简单的例子:packagemainimport"fmt"const(defaultAddr="127.0.0.1"defaultPort=8000)typeServerstruct{AddrstringPortint}funcNewServer()*Server{return&Server{Addr:defaultAddr,Port:defaultPort,}}funcNewServerWithOptions(addr字符串,portint)*Server{return&Server{Addr:addr,Port:port,}}funcmain(){s1:=NewServer()s2:=NewServerWithOptions("localhost",8001)fmt.Println(s1)//&{127.0.0.18000}fmt.Println(s2)//&{localhost8001}}这里我们实现了两个Server结构体的构造函数:NewServer:可以直接返回Server对象NewServerWithOptions:需要传入addr和port二构造服务器对象的参数。如果默认参数创建的对象可以满足要求,不需要自定义Server,我们可以使用NewServer生成对象(s1)。而如果我们需要自定义Server,我们可以使用NewServerWithOptions来生成对象(s2)。通过默认参数选项实现默认参数的另一种方式是为待生成的结构对象定义一个option结构,为待创建的对象生成默认参数。代码实现如下:packagemainimport"fmt"const(defaultAddr="127.0.0.1"defaultPort=8000)typeServerstruct{AddrstringPortint}typeServerOptionsstruct{AddrstringPortint}funcNewServerOptions()*ServerOptions{return&ServerOptions{地址:defaultAddr,端口:defaultPort,}}funithcNewServerWtion(opts*ServerOptions)*Server{return&Server{地址:opts.Addr,端口:opts.Port,}}funcmain(){s1:=NewServerWithOptions(NewServerOptions())s2:=NewServerWithOptions(&ServerOptions{Addr:"localhost",Port:8001,})fmt.Println(s1)//&{127.0.0.18000}fmt.Println(s2)//&{localhost8001}}我们专门为Server结构体实现了一个ServerOptions来生成Default参数,调用NewServerOptions函数获取默认参数配置,构造函数NewServerWithOptions接收一个*ServerOptions类型作为参数。所以我们可以通过以下两种方式完成该功能:直接将调用NewServerOptions函数的返回值传递给NewServerWithOptions通过默认参数实现对象生成(s1)通过手动构造ServerOptions配置生成自定义对象(s2)通过以上实现option方式虽然这两种方法都可以完成功能,但是它们有以下缺点:通过多个构造函数实现的方案需要我们在实例化对象时调用不同的构造函数,代码封装性不强,会增加调用者的负担。默认参数options实现的方案需要我们预先构造一个option结构体,使用默认参数生成对象时代码显得冗余。选项模式可以让我们更优雅地解决这个问题。代码实际如下:packagemainimport"fmt"const(defaultAddr="127.0.0.1"defaultPort=8000)typeServerstruct{AddrstringPortint}typeServerOptionsstruct{AddrstringPortint}typeServerOptioninterface{apply(*ServerOptions)}类型FuncServerOptionstruct{ffunc(*ServerOptions)}func(foFuncServerOption)apply(option*ServerOptions){fo.f(option)}funcWithAddr(addrstring)ServerOption{returnFuncServerOption{f:func(options*ServerOptions){options.Addr=addr},}}funcWithPort(portint)ServerOption{returnFuncServerOption{f:func(options*ServerOptions){options.Port=port},}}funcNewServer(opts...ServerOption)*Server{options:=ServerOptions{Addr:defaultAddr,Port:defaultPort,}for_,opt:=rangeopts{opt.apply(&options)}return&Server{Addr:options.Addr,Port:选项。端口,}}funcmain(){s1:=NewServer()s2:=NewServer(WithAddr("localhost"),WithPort(8001))s3:=NewServer(WithPort(8001))fmt.Println(s1)//&{127.0.0.18000}fmt.Println(s2)//&{localhost8001}fmt.Println(s3)//&{127.0.0.18001}}乍一看我们的代码复杂很多,但实际上调用构造函数生成的对象的代码复杂度没变,只是定义复杂了。我们定义了ServerOptions结构来配置默认参数。因为Addr和Port有默认参数,所以ServerOptions的定义和Server的定义是一样的。但是,在一个复杂的结构中,可能会有一些参数没有默认参数,必须由用户配置。这时候ServerOptions中的字段会比较少,你可以根据需要自行定义。同时,我们还定义了一个ServerOption接口和一个实现该接口的FuncServerOption结构体。它们的作用是允许我们通过apply方法单独为ServerOptions结构体配置一个参数。我们可以为每个默认参数定义一个WithXXX函数来配置参数,比如这里定义的WithAddr和WithPort,这样用户就可以通过调用WithXXX函数来自定义需要覆盖的默认参数。此时,默认参数定义在构造函数NewServer中。构造函数接收一个可变长度的参数,类型为ServerOption。在构造函数内部,通过for循环调用每个传入的ServerOption对象的apply方法,将用户配置的参数依次赋值给构造函数内部的默认参数对象options,以替换默认参数。for循环执行后,得到的options对象就是最终的配置,依次将其属性赋值给Server,生成一个新的对象。总结s2和s3的打印结果,可以发现使用option方式实现的构造函数更加灵活。与前两种实现相比,在选项模式下,我们可以随意更改任意一个或多个默认配置。虽然选项模式确实需要更多代码,但在大多数情况下这是值得的。例如,在Google的gRPC框架的Go语言实现中,创建gRPC服务器的构造函数NewServer就使用了option方式。有兴趣的同学可以看看它的源码。实现思路其实和这里的示例程序是一模一样的。以上就是我对Golang的option模式的一点体会。希望今天的分享能给大家带来一些帮助。推荐阅读服务端渲染基础云原生灰度更新实践
