Go創(chuàng)建對(duì)象時(shí),如何優(yōu)雅的傳遞初始化參數(shù)?這里所說(shuō)的優(yōu)雅,指的是:
- 支持傳遞多個(gè)參數(shù)
- 參數(shù)個(gè)數(shù)、類型發(fā)生變化時(shí),盡量保持接口的兼容性
- 參數(shù)支持默認(rèn)值
- 具體的參數(shù)可根據(jù)調(diào)用方需關(guān)心的程度,決定是否提供默認(rèn)值
Go并不像c++和python那樣,支持函數(shù)默認(rèn)參數(shù)。所以使用Go時(shí),我們需要一種方便、通用的手法來(lái)完成這件事。
Go的很多開(kāi)源項(xiàng)目都使用Option模式,但各自的實(shí)現(xiàn)可能有些許細(xì)微差別。
本文將通過(guò)一個(gè)漸進(jìn)式的demo示例來(lái)介紹Option模式,以及相關(guān)的一些思考。本文將內(nèi)容切分為10個(gè)小模塊,如果覺(jué)得前面的鋪墊冗余,想直接看Option模式的介紹,可以從小標(biāo)題七開(kāi)始閱讀。
零
先看demo,一開(kāi)始我們的代碼是這樣的:
type Foo struct {
num int
str string
// ...
}
func New(num int, str string) *Foo {
// ...
return &Foo{
num: num,
str: str,
}
}
// ...
我們有一個(gè)Foo結(jié)構(gòu)體,內(nèi)部有num和str兩個(gè)屬性,New函數(shù)傳入兩個(gè)初始化參數(shù),構(gòu)造一個(gè)Foo對(duì)象。
ok,一切都足夠簡(jiǎn)單。
假設(shè)我們需要對(duì)Foo內(nèi)部增加兩個(gè)屬性,同時(shí)構(gòu)造函數(shù)也需要支持傳入這兩個(gè)新增屬性的初始值。有一種修改方法是這樣的:
func New(num int, str string, num2 int, str2 string)
可以看到,這種方式,隨著初始化參數(shù)個(gè)數(shù)、類型的變化,我們New函數(shù)的函數(shù)簽名也需隨之改變。這帶來(lái)兩個(gè)壞處:
- 對(duì)調(diào)用方來(lái)說(shuō),函數(shù)不兼容
- 參數(shù)數(shù)量太多,可讀性可能變差
一
有一種保持兼容性的解決方案,是保留之前的New函數(shù),再創(chuàng)建一個(gè)新的構(gòu)造函數(shù),比如New2,用于實(shí)現(xiàn)4個(gè)參數(shù)的構(gòu)造方法。
這種解決方案在大部分時(shí)候會(huì)導(dǎo)致代碼可維護(hù)性下降。
二
另一種解決方案,是把所有的參數(shù)都放入一個(gè)結(jié)構(gòu)體中。就像這樣:
type Foo struct {
option Option
// ...
}
type Option struct {
num int
str string
}
func New(option Option) *Foo {
// ...
return &Foo{
option: option,
}
}
這種方式,解決了上面提出的兩個(gè)問(wèn)題。但是,假設(shè)我們想為參數(shù)提供默認(rèn)參數(shù)呢?
比如說(shuō)當(dāng)調(diào)用方不設(shè)置num時(shí),我們希望它的默認(rèn)值是100;不設(shè)置str時(shí),默認(rèn)值為hello。
三
// 構(gòu)造對(duì)象時(shí)只設(shè)置 str,不設(shè)置 num
foo := New(Option{
str: "world",
})
這種做法可行的前提是,屬性的默認(rèn)值也為0值。
假設(shè)我們希望option.num屬性默認(rèn)值是100,那么當(dāng)內(nèi)部接收到的option.num為0時(shí),我們沒(méi)法區(qū)分是調(diào)用方希望將option.num設(shè)置為0,還是調(diào)用方壓根就沒(méi)設(shè)置option.num。從而導(dǎo)致我們不知道將內(nèi)部的option.num設(shè)置為0好,還是保持默認(rèn)值100好。
事實(shí)上,這個(gè)問(wèn)題不僅僅是傳遞Option時(shí)才會(huì)出現(xiàn),即使所有參數(shù)都使用最上面那種直接傳遞的方式,也會(huì)存在這個(gè)問(wèn)題,即0值無(wú)法作為外部是否設(shè)置的判斷條件。
四
有一種解決方案,是使用*Option即指針類型作為初始化參數(shù),如果外部傳入為nil,則使用默認(rèn)參數(shù)。代碼如下:
func New(option *Option) *Foo {
if option == nil {
// 外部沒(méi)有設(shè)置參數(shù)
}
}
該方案存在的問(wèn)題是,所有的參數(shù)要么全部由外部傳入,要么全部使用默認(rèn)值。
如何才能細(xì)化到每一個(gè)具體的參數(shù),外部設(shè)置了使用外部設(shè)置的值,外部沒(méi)有設(shè)置則使用默認(rèn)值呢?
五
一種解決方案,是Option中的所有屬性,都使用指針類型,如果特定參數(shù)為nil,則該參數(shù)使用默認(rèn)參數(shù)。代碼如下:
type Option struct {
num *int
str *string
}
func New(option Option) *Foo {
if option.num == nil {
// num 使用默認(rèn)值
} else {
// option.num 即為調(diào)用方設(shè)置的初始值
}
// ...
}
該方案存在的問(wèn)題是,對(duì)于調(diào)用方來(lái)說(shuō),使用起來(lái)有些反人類,因?yàn)槟銦o(wú)法使用類似&1的寫法對(duì)一個(gè)整型字面常量取地址,這意味著調(diào)用方必須格外定義一個(gè)變量保存他需要設(shè)置的參數(shù)的值,然后再對(duì)這個(gè)變量取地址賦值給Option的屬性。代碼如下:
// // 下面這種寫法會(huì)造成編譯錯(cuò)誤
// option := {
// num: &200,
// str: &"world",
// }
//
// // 只能這樣寫
// num := 200
// str := "world"
// option := {
// num: &num,
// str: &str,
// }
// foo := New(option)
看起來(lái)有點(diǎn),額,不太優(yōu)雅。
六
另一種值得一提的解決方案,是使用Go可變參數(shù)的特性。代碼如下:
func New(num int, str string, num2 ...int) {
if len(num2) == 0 {
// 調(diào)用方?jīng)]有設(shè)置 num2,內(nèi)部的 num2 應(yīng)使用默認(rèn)值
} else {
// num2[0] 即為調(diào)用方設(shè)置的初始值
}
}
該方案存在的問(wèn)題是,只能有一個(gè)參數(shù)有默認(rèn)值。
七
ok,說(shuō)了這么多,是時(shí)候開(kāi)始上主菜了。Go是支持頭等函數(shù)的語(yǔ)言,即可以將函數(shù)作為變量傳遞。所以我們可以像下面這樣寫:
type Option struct {
num int
str string
}
type ModOption func(option *Option)
func New(modOption ModOption) *Foo {
// 默認(rèn)值
option := Option{
num: 100,
str: "hello",
}
modOption(&option)
return &Foo{
option: option,
}
}
我們的New函數(shù)不再直接接收Option的值,而是提供了一種類似于鉤子函數(shù)的功能,使得在內(nèi)部對(duì)option設(shè)置完默認(rèn)值之后,調(diào)用方可以直接選擇修改哪些屬性。比如調(diào)用方只設(shè)置num,代碼如下:
New(func(option *Option) {
// 調(diào)用方只設(shè)置 num
option.num = 200
})
八
那么假設(shè)有些時(shí)候,我們覺(jué)得某個(gè)參數(shù)是調(diào)用方必須關(guān)心的,不應(yīng)該由內(nèi)部設(shè)置默認(rèn)值呢?我們可以這樣寫:
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) {
// 調(diào)用方只設(shè)置 num
option.num = 200
})
}
九
最后再來(lái)一種常見(jiàn)的、高級(jí)點(diǎn)的寫法。在上面代碼的基礎(chǔ)上,增加如下代碼:
func WithNum(num int) ModOption {
return func(option *Option) {
option.num = num
}
}
func WithStr(str string) ModOption {
return func(option *Option) {
option.str = str
}
}
然后是調(diào)用方的代碼:
// 可以這樣寫
foo := New("iamkey", WithNum(200))
// 還可以這樣寫
foo := New("iamkey", WithStr("world"))
能不能兩個(gè)一起用呢?其實(shí)是可以的,結(jié)合我們上文講到的可變參數(shù),將New函數(shù)修改如下:
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"))
總結(jié)
至此,關(guān)于Option模式的介紹就結(jié)束啦。
事實(shí)上,Option模式除了在創(chuàng)建對(duì)象時(shí)可以使用,里面的一些API設(shè)計(jì)思想,Go的小技巧,在編寫普通函數(shù)時(shí)也可以使用。
模式說(shuō)白了就是一種套路。在實(shí)現(xiàn)功能的基礎(chǔ)之上,大家都熟悉了某種固有套路的寫法,都按著這個(gè)套路走,那么代碼的可讀性、可維護(hù)性就更高些。
對(duì)于一個(gè)特定場(chǎng)景,沒(méi)有最好的模式,只有最適合的模式。不要過(guò)度設(shè)計(jì),手里就一把錘子,瞅啥都是釘子。
舉個(gè)例子,最后說(shuō)的那種WithXXX寫法,我個(gè)人認(rèn)為在大部分時(shí)候都有點(diǎn)皮褲套棉褲,簡(jiǎn)單事情復(fù)雜化的感覺(jué),不如只用一個(gè)ModOption直接修改option來(lái)得簡(jiǎn)單、直觀,所以我?guī)缀醪挥?code>WithXXX的寫法。但是在有些場(chǎng)景,你如果覺(jué)得提供WithXXX對(duì)調(diào)用方更友好,那么用用也挺好。
為了保持場(chǎng)景的純粹性,上面的demo可能會(huì)有些抽象。如果你想進(jìn)一步看看Option模式在實(shí)際項(xiàng)目中是如何使用的,可以看看我的這個(gè)開(kāi)源項(xiàng)目:naza。該項(xiàng)目在構(gòu)造對(duì)象時(shí)大量使用了Option模式。比如 consistenthash.go, bitrate.go 等等。并且做了一些私人化的風(fēng)格規(guī)范。
最后,感謝閱讀,如果覺(jué)得文章還不錯(cuò),可以給我的github項(xiàng)目naza 來(lái)個(gè)star哈。該項(xiàng)目是我學(xué)習(xí)Go時(shí)寫的一些輪子代碼集合,后續(xù)我還會(huì)寫一些文章逐個(gè)介紹里面的輪子以及一些寫Go代碼的技巧。
naza項(xiàng)目地址: https://github.com/q191201771/naza
naza的其他的文章:
原文鏈接: https://pengrl.com/p/60015/
原文出處: yoko blog (https://pengrl.com)
原文作者: yoko
版權(quán)聲明: 本文歡迎任何形式轉(zhuǎn)載,轉(zhuǎn)載時(shí)完整保留本聲明信息(包含原文鏈接、原文出處、原文作者、版權(quán)聲明)即可。本文后續(xù)所有修改都會(huì)第一時(shí)間在原始地址更新。
本篇文章由一文多發(fā)平臺(tái)ArtiPub自動(dòng)發(fā)布