熟悉 Python 开发的同学都知道,Python 有默认参数的存在,使得我们在实例化一个对象的时候,可以根据需要来选择性的覆盖某些默认参数,以此来决定如何实例化对象。当一个对象有多个默认参数时,这个特性非常好用,能够优雅地简化代码。

而 Go 语言从语法上是不支持默认参数的,所以为了实现既能通过默认参数创建对象,又能通过传递自定义参数创建对象,我们就需要通过一些编程技巧来实现。对于这些程序开发中的常见问题,软件行业的先行者们总结了许多解决常见场景编码问题的最佳实践,这些最佳实践后来成为了我们所说的设计模式。其中选项模式在 Go 语言开发中会经常用到。

通常我们有以下三种方法来实现通过默认参数创建对象,以及通过传递自定义参数创建对象:

  • 使用多个构造函数

  • 默认参数选项

  • 选项模式

通过多构造函数实现

第一种方式是通过多构造函数实现,下面是一个简单例子:

package main

import "fmt"

const (
defaultAddr = "127.0.0.1"
defaultPort = 8000
) type Server struct {
Addr string
Port int
} func NewServer() *Server {
return &Server{
Addr: defaultAddr,
Port: defaultPort,
}
} func NewServerWithOptions(addr string, port int) *Server {
return &Server{
Addr: addr,
Port: port,
}
} func main() {
s1 := NewServer()
s2 := NewServerWithOptions("localhost", 8001)
fmt.Println(s1) // &{127.0.0.1 8000}
fmt.Println(s2) // &{localhost 8001}
}

这里我们为 Server 结构体实现了两个构造函数:

  • NewServer:无需传递参数即可直接返回 Server 对象

  • NewServerWithOptions :需要传递 addr 和 port 两个参数来构造 Server 对象

如果通过默认参数创建的对象即可满足需求,不需要对 Server 进行定制时,我们可以使用 NewServer 来生成对象(s1)。而如果需要对 Server 进行定制时,我们则可以使用 NewServerWithOptions 来生成对象(s2)。

通过默认参数选项实现

另外一种实现默认参数的方案,是为要生成的结构体对象定义一个选项结构体,用来生成要创建对象的默认参数,代码实现如下:

package main

import "fmt"

const (
defaultAddr = "127.0.0.1"
defaultPort = 8000
) type Server struct {
Addr string
Port int
} type ServerOptions struct {
Addr string
Port int
} func NewServerOptions() *ServerOptions {
return &ServerOptions{
Addr: defaultAddr,
Port: defaultPort,
}
} func NewServerWithOptions(opts *ServerOptions) *Server {
return &Server{
Addr: opts.Addr,
Port: opts.Port,
}
} func main() {
s1 := NewServerWithOptions(NewServerOptions())
s2 := NewServerWithOptions(&ServerOptions{
Addr: "localhost",
Port: 8001,
})
fmt.Println(s1) // &{127.0.0.1 8000}
fmt.Println(s2) // &{localhost 8001}
}

我们为 Server 结构体专门实现了一个 ServerOptions 用来生成默认参数,调用 NewServerOptions 函数即可获得默认参数配置,构造函数 NewServerWithOptions 接收一个 *ServerOptions 类型作为参数。所以我们可以通过以下两种方式来完成功能:

  • 直接将调用 NewServerOptions 函数的返回值传递给 NewServerWithOptions 来实现通过默认参数生成对象(s1)

  • 通过手动构造 ServerOptions 配置来生成定制对象(s2)

通过选项模式实现

以上两种方式虽然都能够完成功能,但却有以下缺点:

  • 通过多构造函数实现的方案需要我们在实例化对象时分别调用不同的构造函数,代码封装性不强,会给调用者增加使用负担。

  • 通过默认参数选项实现的方案需要我们预先构造一个选项结构,当使用默认参数生成对象时代码看起来比较冗余。

而选项模式可以让我们更为优雅地解决这个问题。代码实现如下:

package main

import "fmt"

const (
defaultAddr = "127.0.0.1"
defaultPort = 8000
) type Server struct {
Addr string
Port int
} type ServerOptions struct {
Addr string
Port int
} type ServerOption interface {
apply(*ServerOptions)
} type FuncServerOption struct {
f func(*ServerOptions)
} func (fo FuncServerOption) apply(option *ServerOptions) {
fo.f(option)
} func WithAddr(addr string) ServerOption {
return FuncServerOption{
f: func(options *ServerOptions) {
options.Addr = addr
},
}
} func WithPort(port int) ServerOption {
return FuncServerOption{
f: func(options *ServerOptions) {
options.Port = port
},
}
} func NewServer(opts ...ServerOption) *Server {
options := ServerOptions{
Addr: defaultAddr,
Port: defaultPort,
} for _, opt := range opts {
opt.apply(&options)
} return &Server{
Addr: options.Addr,
Port: options.Port,
}
} func main() {
s1 := NewServer()
s2 := NewServer(WithAddr("localhost"), WithPort(8001))
s3 := NewServer(WithPort(8001))
fmt.Println(s1) // &{127.0.0.1 8000}
fmt.Println(s2) // &{localhost 8001}
fmt.Println(s3) // &{127.0.0.1 8001}
}

乍一看我们的代码复杂了很多,但其实调用构造函数生成对象的代码复杂度是没有改变的,只是定义上的复杂。

我们定义了 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 的打印结果可以发现,使用选项模式实现的构造函数更加灵活,相较于前两种实现,选项模式中我们可以自由的更改其中任意一项或多项默认配置。

虽然选项模式确实会多写一些代码,但多数情况下这都是值得的。比如 Google 的 gRPC 框架 Go 语言实现中创建 gRPC server 的构造函数 NewServer 就使用了选项模式,感兴趣的同学可以看下其源码的实现思想其实和这里的示例程序如出一辙。

以上就是我关于 Golang 选项模式的一点经验,希望今天的分享能够给你带来一些帮助。

推荐阅读

服务端渲染基础

云原生灰度更新实践

Golang 常见设计模式之选项模式的更多相关文章

  1. Golang 常见设计模式之单例模式

    之前我们已经看过了 Golang 常见设计模式中的装饰和选项模式,今天要看的是 Golang 设计模式里最简单的单例模式.单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在.根据这一特 ...

  2. Java常见设计模式之工厂模式

    工厂模式在我们日常的应用中应当算是比较广泛的一种设计模式了.今天让我们一起来学习一下,工厂的设计模式. 工厂模式在<Java与模式>中分为三类:     1)简单工厂模式(Simple F ...

  3. Java常见设计模式之代理模式

    指由一个代理主题来操作真实主题,真实主题执行具体的业务操作,而代理主题负责其它相关业务的处理.比如生活中的通过代理访问网络,客户通过网络代理连接网络(具体业务),由代理服务器完成用户权限和访问限制等与 ...

  4. Golang 常见设计模式之装饰模式

    想必只要是熟悉 Python 的同学对装饰模式一定不会陌生,这类 Python 从语法上原生支持的装饰器,大大提高了装饰模式在 Python 中的应用.尽管 Go 语言中装饰模式没有 Python 中 ...

  5. Go语言设计模式之函数式选项模式

    Go语言设计模式之函数式选项模式 本文主要介绍了Go语言中函数式选项模式及该设计模式在实际编程中的应用. 为什么需要函数式选项模式? 最近看go-micro/options.go源码的时候,发现了一段 ...

  6. JavaScript 中常见设计模式整理

    开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式.本文意在梳理常见设计模式的特点,从而对它们有比较清晰的认知. JavaScript 中常见设计模 ...

  7. JS中常见设计模式总结

    github: https://github.com/14glwu/FEInterviewBox/tree/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F ...

  8. asp.net core 3.0 选项模式1:使用

    本篇只是从应用角度来说明asp.net core的选项模式,下一篇会从源码来分析 1.以前的方式 以前我们使用web.config/app.config时是这样使用配置的 var count = Co ...

  9. Golang设计模式—简单工厂模式(Simple Factory Pattern)

    Golang设计模式--简单工厂模式 背景 假设我们在做一款小型翻译软件,软件可以将德语.英语.日语都翻译成目标中文,并显示在前端. 思路 我们会有三个具体的语言翻译结构体,或许以后还有更多,但现在分 ...

随机推荐

  1. 采集 base64 编码的图片

    问题 爬虫抓取网页的时候,遇到有的图片是 base64 编码的格式,要怎样下载到本地呢? 示例:base64 编码的 img 标签 <!-- 内容太长省略一部分 --> <img s ...

  2. 用 shell 脚本做自动化测试

    前言 项目中有一个功能,需要监控本地文件系统的变更,例如文件的增.删.改名.文件数据变动等等.之前只在 windows 上有实现,采用的是 iocp + ReadDirectoryChanges 方案 ...

  3. 手动上下eureka上面服务

    手动下eureka curl -X PUT http://eureka.xxx.xxx.com/eureka/apps/VIDEO-API/111.111.111.111:test-api:0000/ ...

  4. java 编程基础:注解(Annotation Processing Tool)注解处理器 利用注解解读类属性生成XML文件

    APT的介绍: APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注解信息进行额外的处理. 使用APT工具 ...

  5. redis集群搭建,使用注意

    https://www.cnblogs.com/vieta/p/11192137.html https://blog.csdn.net/qq_42815754/article/details/8291 ...

  6. div中出现滚动条,自动保持在最底端---显示聊天窗口最新的信息

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. Java 将PDF转为线性PDF

    线性化PDF文件是PDF文件的一种特殊格式,可以通过Internet更快地进行查看.线性化的PDF,在页面数量很多的情况下,更能突出表现出快速浏览的优势.下面是通过后端Java程序实现将PDF文件转为 ...

  8. Centos 配置服务器

    Centos 配置服务器 (配置服务器 除了Git Bash Here 还可以安装Xshell 网址:https://xshell.en.softonic.com/   Wincp 网址:https: ...

  9. PHP伪协议-文件包含

    lfi.php案例代码 <?php include $_GET['file']; ?> phar://.zip://.zlib://   用于读取压缩文件,zip:// .phart:// ...

  10. Android NDK开发篇:Java与原生代码通信(原生方法声明与定义与数据类型)

    Java与原生代码通信涉及到原生方法声明与定义.数据类型.引用数据类型操作.NIO操作.访问域.异常处理.原生线程 1.原生方法声明与定义 关于原生方法的声明与定义在上一篇已经讲一点了,这次详细分析一 ...