Go創(chuàng)建對(duì)象時(shí),如何優(yōu)雅的傳遞初始化參數(shù)

Go創(chuàng)建對(duì)象時(shí),如何優(yōu)雅的傳遞初始化參數(shù)?這里所說(shuō)的優(yōu)雅,指的是:

  1. 支持傳遞多個(gè)參數(shù)
  2. 參數(shù)個(gè)數(shù)、類型發(fā)生變化時(shí),盡量保持接口的兼容性
  3. 參數(shù)支持默認(rèn)值
  4. 具體的參數(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)部有numstr兩個(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è)壞處:

  1. 對(duì)調(diào)用方來(lái)說(shuō),函數(shù)不兼容
  2. 參數(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.num0時(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ā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,707評(píng)論 0 5
  • 函數(shù)和對(duì)象 1、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語(yǔ)言來(lái)說(shuō)都是核心的概念。通過(guò)函數(shù)可以封裝任意多條語(yǔ)句,而且...
    道無(wú)虛閱讀 4,944評(píng)論 0 5
  • ORA-00001: 違反唯一約束條件 (.) 錯(cuò)誤說(shuō)明:當(dāng)在唯一索引所對(duì)應(yīng)的列上鍵入重復(fù)值時(shí),會(huì)觸發(fā)此異常。 O...
    我想起個(gè)好名字閱讀 5,947評(píng)論 0 9
  • 第3章 基本概念 3.1 語(yǔ)法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡(jiǎn)單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,503評(píng)論 0 21
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,243評(píng)論 0 38

友情鏈接更多精彩內(nèi)容