Go创建对象时,如何优雅的传递初始化参数
Go
创建对象时,如何优雅的传递初始化参数?这里所说的优雅,指的是:
- 支持传递多个参数
- 参数个数、类型发生变化时,尽量保持接口的兼容性
- 参数支持默认值
- 具体的参数可根据调用方需关心的程度,决定是否提供默认值
Go
并不像 c++
和 python
那样,支持函数默认参数。所以使用 Go
时,我们需要一种方便、通用的手法来完成这件事。
Go
的很多开源项目都使用 Option
模式,但各自的实现可能有些许细微差别。
本文将通过一个渐进式的 demo
示例来介绍 Option
模式,以及相关的一些思考。 本文将内容切分为10个小模块,如果觉得前面的铺垫冗余,想直接看Option模式的介绍,可以从小标题七开始阅读。
零
先看 demo
,一开始我们的代码是这样的:
type Foo struct { num int str string // ... } func New(num int, str string) *Foo { // ... return &Foo{ num: num, str: str, } } // ...
我们有一个 Foo
结构体,内部有 num
和 str
两个属性, New
函数传入两个初始化参数,构造一个 Foo
对象。
ok,一切都足够简单。
假设我们需要对 Foo
内部增加两个属性,同时构造函数也需要支持传入这两个新增属性的初始值。有一种修改方法是这样的:
func New(num int, str string, num2 int, str2 string)
可以看到,这种方式,随着初始化参数个数、类型的变化,我们 New
函数的函数签名也需随之改变。这带来两个坏处:
- 对调用方来说,函数不兼容
- 参数数量太多,可读性可能变差
一
有一种保持兼容性的解决方案,是保留之前的 New
函数,再创建一个新的构造函数,比如 New2
,用于实现4个参数的构造方法。
这种解决方案在大部分时候会导致代码可维护性下降。
二
另一种解决方案,是把所有的参数都放入一个结构体中。就像这样:
type Foo struct { option Option // ... } type Option struct { num int str string } func New(option Option) *Foo { // ... return &Foo{ option: option, } }
这种方式,解决了上面提出的两个问题。但是,假设我们想为参数提供默认参数呢?
比如说当调用方不设置 num
时,我们希望它的默认值是 100
;不设置 str
时,默认值为 hello
。
三
// 构造对象时只设置 str,不设置 num foo := New(Option{ str: "world", })
这种做法可行的前提是,属性的默认值也为 0
值。
假设我们希望 option.num
属性默认值是 100
,那么当内部接收到的 option.num
为 0
时,我们没法区分是调用方希望将 option.num
设置为 0
,还是调用方压根就没设置 option.num
。从而导致我们不知道将内部的 option.num
设置为 0
好,还是保持默认值 100
好。
事实上,这个问题不仅仅是传递 Option
时才会出现,即使所有参数都使用最上面那种直接传递的方式,也会存在这个问题,即 0
值无法作为外部是否设置的判断条件。
四
有一种解决方案,是使用 *Option
即指针类型作为初始化参数,如果外部传入为 nil
,则使用默认参数。代码如下:
func New(option *Option) *Foo { if option == nil { // 外部没有设置参数 } }
该方案存在的问题是,所有的参数要么全部由外部传入,要么全部使用默认值。
如何才能细化到每一个具体的参数,外部设置了使用外部设置的值,外部没有设置则使用默认值呢?
五
一种解决方案,是 Option
中的所有属性,都使用指针类型,如果特定参数为 nil
,则该参数使用默认参数。代码如下:
type Option struct { num *int str *string } func New(option Option) *Foo { if option.num == nil { // num 使用默认值 } else { // option.num 即为调用方设置的初始值 } // ... }
该方案存在的问题是,对于调用方来说,使用起来有些反人类,因为你无法使用类似 &1
的写法对一个整型字面常量取地址,这意味着调用方必须格外定义一个变量保存他需要设置的参数的值,然后再对这个变量取地址赋值给Option的属性。代码如下:
// // 下面这种写法会造成编译错误 // option := { // num: &200, // str: &"world", // } // // // 只能这样写 // num := 200 // str := "world" // option := { // num: &num, // str: &str, // } // foo := New(option)
看起来有点,额,不太优雅。
六
另一种值得一提的解决方案,是使用 Go
可变参数的特性。代码如下:
func New(num int, str string, num2 ...int) { if len(num2) == 0 { // 调用方没有设置 num2,内部的 num2 应使用默认值 } else { // num2[0] 即为调用方设置的初始值 } }
该方案存在的问题是,只能有一个参数有默认值。
七
ok,说了这么多,是时候开始上主菜了。 Go
是支持头等函数的语言,即可以将函数作为变量传递。所以我们可以像下面这样写:
type Option struct { num int str string } type ModOption func(option *Option) func New(modOption ModOption) *Foo { // 默认值 option := Option{ num: 100, str: "hello", } modOption(&option) return &Foo{ option: option, } }
我们的 New
函数不再直接接收 Option
的值,而是提供了一种类似于钩子函数的功能,使得在内部对 option
设置完默认值之后,调用方可以直接选择修改哪些属性。比如调用方只设置 num
,代码如下:
New(func(option *Option) { // 调用方只设置 num option.num = 200 })
八
那么假设有些时候,我们觉得某个参数是调用方必须关心的,不应该由内部设置默认值呢?我们可以这样写:
package main type Foo struct { key string option Option // ... } type Option struct { num int str string } type ModOption func(option *Option) func New(key string, modOption ModOption) *Foo { option := Option{ num: 100, str: "hello", } modOption(&option) return &Foo{ key: key, option: option, } } // ... func main() { New("iamkey", func(option *Option) { // 调用方只设置 num option.num = 200 }) }
九
最后再来一种常见的、高级点的写法。在上面代码的基础上,增加如下代码:
func WithNum(num int) ModOption { return func(option *Option) { option.num = num } } func WithStr(str string) ModOption { return func(option *Option) { option.str = str } }
然后是调用方的代码:
// 可以这样写 foo := New("iamkey", WithNum(200)) // 还可以这样写 foo := New("iamkey", WithStr("world"))
能不能两个一起用呢?其实是可以的,结合我们上文讲到的可变参数,将 New
函数修改如下:
func New(key string, modOptions ...ModOption) *Foo { option := Option{ num: 100, str: "hello", } for _, fn := range modOptions { fn(&option) } return &Foo{ key: key, option: option, } }
然后是使用方的代码:
New("iamkey", WithNum(200), WithStr("world"))
总结
至此,关于 Option
模式的介绍就结束啦。
事实上, Option
模式除了在创建对象时可以使用,里面的一些 API
设计思想, Go
的小技巧,在编写普通函数时也可以使用。
模式说白了就是一种套路。在实现功能的基础之上,大家都熟悉了某种固有套路的写法,都按着这个套路走,那么代码的可读性、可维护性就更高些。
对于一个特定场景,没有最好的模式,只有最适合的模式。不要过度设计,手里就一把锤子,瞅啥都是钉子。
举个例子,最后说的那种 WithXXX
写法,我个人认为在大部分时候都有点皮裤套棉裤,简单事情复杂化的感觉,不如只用一个 ModOption
直接修改 option
来得简单、直观,所以我几乎不用 WithXXX
的写法。但是在有些场景,你如果觉得提供 WithXXX
对调用方更友好,那么用用也挺好。
为了保持场景的纯粹性,上面的 demo
可能会有些抽象。如果你想进一步看看 Option
模式在实际项目中是如何使用的,可以看看我的这个开源项目: naza
。该项目在构造对象时大量使用了 Option
模式。比如 consistenthash.go
, bitrate.go
等等。并且做了一些私人化的风格规范。
最后,感谢阅读,如果觉得文章还不错,可以给我的 github
项目 naza
来个 star
哈。该项目是我学习 Go
时写的一些轮子代码集合,后续我还会写一些文章逐个介绍里面的轮子以及一些写 Go
代码的技巧。
naza
项目地址: https://github.com/q191201771/naza
naza
的其他的文章:
原文链接: https://pengrl.com/p/60015/
原文出处: yoko blog
( https://pengrl.com
)
原文作者:yoko
版权声明:本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。