<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          簡(jiǎn)單易懂的 Go 泛型使用和實(shí)現(xiàn)原理介紹

          共 4694字,需瀏覽 10分鐘

           ·

          2022-05-15 14:43

          原文:A gentle introduction to generics in Go by Dominik Braun

          萬(wàn)俊峰Kevin:我看了覺得文章非常簡(jiǎn)單易懂,就征求了作者同意,翻譯出來(lái)給大家分享一下。

          本文是對(duì)泛型的基本思想及其在 Go 中的實(shí)現(xiàn)的一個(gè)比較容易理解的介紹,同時(shí)也是對(duì)圍繞泛型的各種性能討論的簡(jiǎn)單總結(jié)。首先,我們來(lái)看看泛型所解決的核心問(wèn)題。

          問(wèn)題

          假設(shè)我們想實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 tree 數(shù)據(jù)結(jié)構(gòu)。每個(gè)節(jié)點(diǎn)持有一個(gè)值。在 Go 1.18 之前,實(shí)現(xiàn)這種結(jié)構(gòu)的典型方法如下。

          type?Node?struct?{
          ????value?interface{}
          }

          這在大多數(shù)情況下都很好用,但它也有一些缺點(diǎn)。

          首先,interface{} 可以是任何東西。如果我們想限制 value 可能持有的類型,例如整數(shù)和浮點(diǎn)數(shù),我們只能在運(yùn)行時(shí)檢查這個(gè)限制。

          func?(n?Node)?IsValid()?bool?{
          ????switch?n.value.(type)?{
          ????????case?int,?float32,?float64:
          ????????????return?true
          ????????default:
          ????????????return?false
          ????}
          }

          這樣并不可能在編譯時(shí)限制類型,像上面這樣的類型判斷在許多 Go 庫(kù)中都是很常見的做法。這里有 go-zero 項(xiàng)目中的例子。

          第二,對(duì) Node 中的值進(jìn)行處理是非常繁瑣和容易出錯(cuò)的。對(duì)值做任何事情都會(huì)涉及到某種類型的斷言,即使你可以安全地假設(shè)值持有一個(gè) int 值。

          number,?ok?:=?node.value.(int)
          if?!ok?{
          ????//?...
          }

          double?:=?number?*?2

          這些只是使用 interface{} 的一些不便之處,它沒有提供類型安全,并有可能導(dǎo)致難以恢復(fù)的運(yùn)行時(shí)錯(cuò)誤。

          解決方法

          我們不打算接受任意數(shù)據(jù)類型或具體類型,而是定義一個(gè)叫做 T占位符類型 作為值的類型。請(qǐng)注意,這段代碼還不會(huì)通過(guò)編譯。

          type?Node[T]?struct?{
          ????value?T
          }

          首先需要聲明泛型類型 T,這是在結(jié)構(gòu)或函數(shù)名稱后面方括號(hào)里面使用的。

          T 可以是任何類型,只有在實(shí)例化一個(gè)具有明確類型的 Node 時(shí),T 才會(huì)被推導(dǎo)為該類型。

          n?:=?Node[int]{
          ????value:?5,
          }

          泛型 Node 被實(shí)例化為 Node[int](整數(shù)節(jié)點(diǎn)),所以 T 是一個(gè) int

          類型約束

          上面的實(shí)現(xiàn)里,T 的聲明缺少一個(gè)必要的信息:類型約束。

          類型約束用于進(jìn)一步限制可以作為 T 的可能類型。Go 本身提供了一些預(yù)定義的類型約束,但也可以使用自定義的類型約束。

          type?Node[T?any]?struct?{
          ????value?T
          }

          任意類型(any)約束允許 T 實(shí)際上是任何類型。如果節(jié)點(diǎn)值需要進(jìn)行比較,有一個(gè) comparable 類型約束,滿足這個(gè)預(yù)定義約束的類型可以使用 == 進(jìn)行比較。

          type?Node[T?comparable]?struct?{
          ????value?T
          }

          任何類型都可以作為一個(gè)類型約束。Go 1.18 引入了一種新的 interface 語(yǔ)法,可以嵌入其他數(shù)據(jù)類型。

          type?Numeric?interface?{
          ????int?|?float32?|?float64
          }

          這意味著一個(gè)接口不僅可以定義一組方法,還可以定義一組類型。使用 Numeric 接口作為類型約束,意味著值可以是整數(shù)或浮點(diǎn)數(shù)。

          type?Node[T?Numeric]?struct?{
          ????value?T
          }

          重獲類型安全

          相對(duì)于使用 interface{},泛型類型參數(shù)的巨大優(yōu)勢(shì)在于,T 的最終類型在編譯時(shí)就會(huì)被推導(dǎo)出來(lái)。為 T 定義一個(gè)類型約束,完全消除了運(yùn)行時(shí)檢查。如果用作 T 的類型不滿足類型約束,代碼就不會(huì)編譯通過(guò)。

          在編寫泛型代碼時(shí),你可以像已經(jīng)知道 T 的最終類型一樣寫代碼。

          func?(n?Node[T])?Value()?T?{
          ????return?n.value
          }

          上面的函數(shù)返回 n.Value,它的類型是 T。因此,返回值是 T,如果 T 是一個(gè)整數(shù),那么返回類型就已知是 int。因此,返回值可以直接作為一個(gè)整數(shù)使用,不需要任何類型斷言。

          n?:=?Node[int]{
          ????value:?5,
          }

          double?:=?n.Value()?*?2

          在編譯時(shí)恢復(fù)類型安全使 Go 代碼更可靠,更不容易出錯(cuò)。

          泛型使用場(chǎng)景

          Ian Lance Taylor 的 When To Use Generics 中列出了泛型的典型使用場(chǎng)景,歸結(jié)為三種主要情況:

          1. 使用內(nèi)置的容器類型,如 slicesmapschannels
          2. 實(shí)現(xiàn)通用的數(shù)據(jù)結(jié)構(gòu),如 linked listtree
          3. 編寫一個(gè)函數(shù),其實(shí)現(xiàn)對(duì)許多類型來(lái)說(shuō)都是一樣的,比如一個(gè)排序函數(shù)

          一般來(lái)說(shuō),當(dāng)你不想對(duì)你所操作的值的內(nèi)容做出假設(shè)時(shí),可以考慮使用泛型。我們例子中的 Node 并不太關(guān)心它持有的值。

          當(dāng)不同的類型有不同的實(shí)現(xiàn)時(shí),泛型就不是一個(gè)好的選擇。另外,不要把 Read(r io.Reader) 這樣的接口函數(shù)簽名改為 Read[T io.Reader](r T) 這樣的通用簽名。

          性能

          要了解泛型的性能及其在 Go 中的實(shí)現(xiàn),首先需要了解一般情況下實(shí)現(xiàn)泛型的兩種最常見方式。

          這是對(duì)各種性能的深入研究和圍繞它們進(jìn)行的討論的簡(jiǎn)要介紹。你大概率不太需要關(guān)心 Go 中泛型的性能。

          虛擬方法表

          在編譯器中實(shí)現(xiàn)泛型的一種方法是使用 Virtual Method Table。泛型函數(shù)被修改成只接受指針作為參數(shù)的方式。然后,這些值被分配到堆上,這些值的指針被傳遞給泛型函數(shù)。這樣做是因?yàn)橹羔樋雌饋?lái)總是一樣的,不管它指向的是什么類型。

          如果這些值是對(duì)象,而泛型函數(shù)需要調(diào)用這些對(duì)象的方法,它就不能再這樣做了。該函數(shù)只有一個(gè)指向?qū)ο蟮闹羔槪恢浪鼈兊姆椒ㄔ谀睦铩R虼耍枰粋€(gè)可以查詢方法的內(nèi)存地址的表格:Virtual Method Table。這種所謂的動(dòng)態(tài)調(diào)度已經(jīng)被 Go 和 Java 等語(yǔ)言中的接口所使用。

          Virtual Method Table 不僅可以用來(lái)實(shí)現(xiàn)泛型,還可以用來(lái)實(shí)現(xiàn)其他類型的多態(tài)性。然而,推導(dǎo)這些指針和調(diào)用虛擬函數(shù)要比直接調(diào)用函數(shù)慢,而且使用 Virtual Method Table 會(huì)阻止編譯器進(jìn)行優(yōu)化。

          單態(tài)化

          一個(gè)更簡(jiǎn)單的方法是單態(tài)化(Monomorphization),編譯器為每個(gè)被調(diào)用的數(shù)據(jù)類型生成一個(gè)泛型函數(shù)的副本。

          func?max[T?Numeric](a,?b?T)?T?{
          ????//?...
          }

          larger?:=?max(3,?5)

          由于上面顯示的max函數(shù)是用兩個(gè)整數(shù)調(diào)用的,編譯器在對(duì)代碼進(jìn)行單態(tài)化時(shí)將為 int 生成一個(gè) max 的副本。

          func?maxInt(a,?b?int)?int?{
          ????//?...
          }

          larger?:=?maxInt(3,?5)

          最大的優(yōu)勢(shì)是,Monomorphization 帶來(lái)的運(yùn)行時(shí)性能明顯好于使用 Virtual Method Table。直接方法調(diào)用不僅更有效率,而且還能適用整個(gè)編譯器的優(yōu)化鏈。不過(guò),這樣做的代價(jià)是編譯時(shí)長(zhǎng),為所有相關(guān)類型生成泛型函數(shù)的副本是非常耗時(shí)的。

          Go 的實(shí)現(xiàn)

          這兩種方法中哪一種最適合 Go?快速編譯很重要,但運(yùn)行時(shí)性能也很重要。為了滿足這些要求,Go 團(tuán)隊(duì)決定在實(shí)現(xiàn)泛型時(shí)混合兩種方法。

          Go 使用 Monomorphization,但試圖減少需要生成的函數(shù)副本的數(shù)量。它不是為每個(gè)類型創(chuàng)建一個(gè)副本,而是為內(nèi)存中的每個(gè)布局生成一個(gè)副本:intfloat64Node 和其他所謂的 "值類型" 在內(nèi)存中看起來(lái)都不一樣,因此泛型函數(shù)將為所有這些類型復(fù)制副本。

          與值類型相反,指針和接口在內(nèi)存中總是有相同的布局。編譯器將為指針和接口的調(diào)用生成一個(gè)泛型函數(shù)的副本。就像 Virtual Method Table 一樣,泛型函數(shù)接收指針,因此需要一個(gè)表來(lái)動(dòng)態(tài)地查找方法地址。在 Go 實(shí)現(xiàn)中的字典與虛擬方法表的性能特點(diǎn)相同。

          結(jié)論

          這種混合方法的好處是,你在使用值類型的調(diào)用中獲得了 Monomorphization 的性能優(yōu)勢(shì),而只在使用指針或接口的調(diào)用中付出了 Virtual Method Table 的成本。

          在性能討論中經(jīng)常被忽略的是,所有這些好處和成本只涉及到函數(shù)的調(diào)用。通常情況下,大部分的執(zhí)行時(shí)間是在函數(shù)內(nèi)部使用的。調(diào)用方法的性能開銷可能不會(huì)成為性能瓶頸,即使是這樣,也要考慮先優(yōu)化函數(shù)實(shí)現(xiàn),再考慮調(diào)用開銷。

          更多閱讀

          Vicent Marti: Generics can make your Go code slower (PlanetScale)

          Andy Arthur: Generics and Value Types in Golang (Dolthub)

          Virtual method table (Wikipedia)

          Monomorphization (Wikipedia)

          Dynamic dispatch (Wikipedia)

          對(duì)標(biāo)準(zhǔn)庫(kù)的影響

          作為 Go 1.18 的一部分,不改變標(biāo)準(zhǔn)庫(kù) 是一個(gè)謹(jǐn)慎的決定。目前的計(jì)劃是收集泛型的經(jīng)驗(yàn),學(xué)習(xí)如何適當(dāng)?shù)厥褂盟鼈儯⒃跇?biāo)準(zhǔn)庫(kù)中找出合理的用例。

          Go 有一些關(guān)于通用包、函數(shù)和數(shù)據(jù)結(jié)構(gòu)的提議:

          • constraints, providing type constraints (#47319)
          • maps, providing generic map functions (#47330)
          • slices, providing generic slice functions (#47203)
          • sort.SliceOf, a generic sort implementation (#47619)
          • sync.PoolOf and other generic concurrent data structures (#47657)

          關(guān)于 go-zero 泛型的計(jì)劃

          對(duì) go-zero 支持用泛型改寫,我們持謹(jǐn)慎態(tài)度,因?yàn)橐坏┦褂梅盒停敲?Go 版本必須從 1.15 升級(jí)到 1.18,很多用戶的線上服務(wù)現(xiàn)在還未升級(jí)到最新版,所以 go-zero 的泛型改寫會(huì)延后 Go 兩三個(gè)版本,確保用戶線上服務(wù)大部分已經(jīng)升級(jí)到 Go 1.18

          go-zero 也在對(duì)泛型做充分的調(diào)研和嘗試。

          其中的 mr 包已經(jīng)新開倉(cāng)庫(kù)支持了泛型:

          https://github.com/kevwan/mapreduce

          其中的 fx 包也已新開倉(cāng)庫(kù)嘗試支持泛型,但是由于缺少 template method,未能完成,期待后續(xù) Go 泛型的完善

          https://github.com/kevwan/stream

          當(dāng)后續(xù) go-zero 支持泛型的時(shí)候,我們就會(huì)合入這些已經(jīng)充分測(cè)試的泛型實(shí)現(xiàn)。

          項(xiàng)目地址

          https://github.com/zeromicro/go-zero

          歡迎使用 go-zerostar 支持我們!



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 14
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲伊人久久久 | 插一插色一色撸一撸视频在线 | 在线免费观看亚洲 | 国产毛片一区二区三区亖区内套视频 | 最新无码国产精品色在线看 |