Golang 泛型初識

本文主要是介紹 Golang 泛型的基本要素,泛型的一些通用代碼的實踐。

分爲以下四個部分

  1. 簡介

  2. 泛型的基本元素

  3. 泛型實踐

  4. 總結

簡介

泛型是什麼?

泛型編程是一種計算機編程風格,編程範式,其中算法是根據稍後指定的類型編寫的,然後在需要時爲作爲參數提供的特定類型實例化。常用的的編程語言也基本都支持泛型這一特性,例如 C++、C#、Java、Python、Rust、Swift、TypeScript、kotlin 等。泛型有以下特點:

Golang 中的泛型

research.swtch.com/generic[1]

普遍的困境是這樣的:你想要慢的程序員、慢的編譯器和臃腫的二進制文件,還是慢的執行時間?——拉斯考克斯(2009 年)

網絡梗圖:手動泛型

很長一段時間以來,Go 都沒有泛型功能,參考爲什麼 Go 語言沒有泛型 - 面向信仰編程 [2] 一文,文中討論到 Golang 沒有支持泛型的原因有兩個:

本文認爲還有一點原因是:

然而在 2020 年年度 Go 開發者調查中,26% 的受訪者表示 Go 缺乏他們需要的語言特性,88% 的受訪者選擇泛型作爲關鍵缺失的特性。

結果來自: Go.dev/blog/survey…[3]

然後在 2021 年 1 月 13 日,Ian Lance Taylor 和 Robert Greseimer(均爲 Go 團隊成員)提議使用類型參數將泛型添加到 Go 中。不過,它並不是突然出現的,之前提出的很多設計都經過討論,最終都進入了這個提案。

github.com/Golang/Go/i…[4]

有關泛型的提案 spec: add generic programming using type parameters #43651[4] 已經被 Go 團隊接受,並在 2022 年 3 月 15 日發佈 Go 1.18 ,此版本 Go 語言發生了重大變化,包括泛型。我們可以基於此特性實現一些有趣的功能,也能擴展代碼的抽象能力、減少部分重複的代碼。

Go 官網博客有兩篇關於泛型的文章、介紹泛型以及何時使用泛型,可作爲初步瞭解 Golang 泛型材料

  • An Introduction To Generics[5]

  • When To Use Generics[6]

與其他語言泛型對比

Go 泛型沒有太多包袱,更多是偏向設計,而 Java、C++ 等老牌語言因兼容或其他原因,慢步迭代過來,這裏對比下 Go 的泛型和老牌語言泛型在語法、類型約束、實現原理等方面的差異:

語法

// Go 泛型泛型語法爲中括號
func Print[T any](t T "T any") {
    fmt.Printf("printing type: %T\n", t)
}
public static <T> void print(T t) {
    System.out.println("printing type: " + t.getClass().getName());
}

類型約束

類型約束在 Java 中支持 Bounds[7](有界與多重有界)即:

  • :是指 “上界通配符(Upper Bounds Wildcards)”

  • :是指 “下界通配符(Lower Bounds Wildcards)”

在 Go 中只包含了對類型或方法的限制。

// 方法限制
type Stringer interface {
   String() string
}
// 類型集合
type Types interface {
   ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 |
      ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64
}

// 限定爲 Types
func  Sub[T Types](t1, t2 T "T Types") T {
   return t1 - t2
}
public class Main{
    // 泛型限定 必須是Collection 類型的子類纔可以被接收
    public static <T extends Collection> void print(T t){
        System.out.println(t);
    }

    public static void main(String[] args){
        print(Arrays.asList(1,2,3));
    }
}

實現原理

Tips:

單態化 [8] 是針對我們要處理的不同類型的數據,多次複製代碼。單態化通常比基於繼承的多態代碼更快,但代價是編譯時間和二進制大小。事實上,單態確保零開銷調用,而基於繼承的多態需要通過虛擬調度表使用間接指針。此外,編譯器可以專門優化和 / 或內聯單態化代碼。

參考泛型實現 [9],Go 泛型實現的一個關鍵特徵是僅部分使用單態化,在 Go 中,單態化只是部分應用了一種稱爲 “GCShape stenciling with Dictionaries[10]” 的技術。這樣做的主要效果是指針類型或接口的所有參數都被視爲屬於相同的底層類型,這意味着只生成該函數的一個單態版本。將此與採用算術參數的函數進行對比,例如int32and float64,每個函數都有自己的專用函數版本。

大多數關於 Java 泛型的抱怨都集中在類型擦除上。此設計沒有類型擦除。泛型類型的反射信息將包括完整的編譯時類型信息。

在 Java 類型通配符 ( List<? extends Number>, List<? super Number>) 中實現協變和逆變。Go 中缺少這些概念,這使得泛型類型變得更加簡單。

C++ 模板不對類型參數實施任何約束(除非採用概念提案)。這意味着更改模板代碼可能會意外破壞遙遠的實例化。這也意味着錯誤消息僅在實例化時報告,並且可能嵌套很深,難以理解。這種設計通過強制和顯式約束避免了這些問題。

C++ 支持模板元編程,可以將其視爲在編譯時使用與非模板 C++ 完全不同的語法完成的普通編程。這種設計沒有類似的特點。這節省了相當多的複雜性,同時損失了一些功率和運行時間效率。

C++ 使用兩階段名稱查找,其中有些名稱在模板定義的上下文中查找,有些名稱在模板實例化的上下文中查找。在這個設計中,所有的名字都是在他們被寫的地方查找的。

實際上,所有 C++ 編譯器都會在實例化每個模板時對其進行編譯。這會減慢編譯時間。這種設計爲如何處理泛型函數的編譯提供了靈活性。

基本元素

類型參數(Type Parameters)

通用代碼是使用開發者稱爲_類型參數_的抽象數據類型編寫的。調用泛型方法時,類型參數將替換爲類型參數。

看一個簡單的例子:

類型參數列表出現在常規參數之前。爲了區分類型參數列表和常規參數列表,類型參數列表使用方括號而不是圓括號。正如常規參數具有類型一樣,類型參數也具有元類型,也稱爲約束。

func Print[T any]([]"T any") {
   for _, v := range s {
      fmt.Println(v)
   }
}

調用泛型方法時:

Print [ int ]([] int { 1 , 2 , 3 } " int ")
// 將會輸出
// 1
// 2
// 3

這裏有一個小細節,Go 中類型參數列表使用的是中括號的語法進行標識,而 Java、C++、Rust 等大多數語言中,使用F<T> 來標識泛型,Go 爲什麼這樣設計呢?

來看這個場景:

a, b = w < x, y > (z)

這段代碼中,如果沒有類型信息,則不能區分右值是一對錶達式w < x , y > (z),還是返回兩個結果值的函數調用,而在此種情況下,Go 希望在沒有類型信息的情況下也能進行正確的解析,那尖括號則滿足不了這個條件。

Go 官方也考慮過F(T)的語法來標識泛型,且早期使用該語法,是可行的,但是此語法引入了一些解析歧義。例如:

var f func(x(T))

此場景有兩種理解:

  1. 單個未命名參數的函數

  2. 以類型x(T)命名參數的函數

這也會給理解上帶來一定的困難,也被否定掉了。

官方還考慮過F<<T>> 但是由於此符號不在 ASCII 中,否定。

Tips:

在當前 Go 版本的實現中,接口值持有實例的指針,將非指針的值傳遞給一個聲明爲interface{}類型的形參,會有一個裝箱的操作,即在內存中,實例的內容在堆棧上,而接口值則是指向實例位置的指針。

但是需要注意的是,在泛型中,泛型類型的值不會被裝箱。

約束(Constraints)

通常,所有泛型代碼都希望類型參數滿足某些要求。這些要求被稱爲約束

看一個例子:

 // 這個方法是無效的
// any 約束並沒有任何可實現的操作(方法)
func Stringify[T any]([]"T any") (ret []string) {
   for _, v := range s {
      ret = append(ret, v.String()) // 編譯錯誤
}
   return ret
}

在此例中:

any約束允許任何類型作爲類型參數,並且只允許函數使用任何類型所允許的操作。其接口類型是空接口:interface{}s切片元素類型爲T ,並且Tany類型的,意味着T類型的實例並沒有強制要求實現String()方法,即上面的代碼將編譯失敗。

所以需要開發者使用合適的約束作用於Stringify,對於調用者傳遞類型參數和泛型函數中的代碼設置限制。調用者只能傳遞滿足約束的類型參數。通用函數只能以約束允許的方式使用這些值。這是一條重要的規則,即:泛型代碼只能使用其類型參數已知可實現的操作。

Go 已經有一個接近於我們需要的約束的構造:接口類型。接口類型是一組方法。唯一可以分配給接口類型變量的值是那些已經實現了全部接口所定義方法的實例。除了對任何類型允許的操作之外,接口變量唯一能做的操作是調用接口定義的方法。

使用類型參數調用泛型函數類似於分配給接口類型的變量:類型參數必須實現類型參數的約束。編寫泛型函數就像使用接口類型的值:泛型代碼只能使用約束允許的操作(或任何類型允許的操作)。

對於上述編譯會失敗的代碼,現在定義一個約束,使得Stringify方法能夠正常編譯通過,並且能夠正常調用。

type Stringer interface {
   String() string
}
func Stringify[T Stringer]([]"T Stringer") (ret []string) {
   for _, v := range s {
      ret = append(ret, v.String())
   }
   return ret
}
type Stringer interface {
   String() string
}

type Plusser interface {
   Plus(string) string
}

func ConcatTo[S Stringer, P Plusser]([]S, p []"S Stringer, P Plusser") []string {
   r := make([]string, len(s))
   for i, v := range s {
      r[i] = p[i].Plus(v.String())
   }
   return r
}

類型集(Type Sets)

類型集是在 Go1.18 擴展的一個概念,不僅僅應用於泛型。在之前版本的 Go 中interface{}可以定義了一組方法。

圖片來自:go.dev/blog/intro-…[11]

在 Go1.18 中 可以將接口看做接口定義了一組類型,即實現這些方法的類型。從這個角度來看,作爲接口類型集元素的任何類型都實現了該接口。

圖片來自:go.dev/blog/intro-…[11]

也可以理解爲 Go 在接口定義中增加了一層抽象去管理不同的方法集,這樣可以更加容易組合不同的類型,使得抽象的操作更加簡便。

約束元素

任意類型約束元素

允許列出任何類型,而不僅僅是接口類型。例:

// 其中 int 爲基礎類型
type Integer  interface { int }

近似約束元素

在日常 coding 中,可能會有很多的類型別名,例如:

type Phone string
type Email string
type Address string
...

此時想對這些類型提供一些通用的處理函數,比如脫敏,這是需要每個類型都去實現一遍方法嗎?並不需要,Go1.18 中擴展了近似約束元素(Approximation constraint element)這個概念,以上述例子來說,即:基礎類型爲 string 的類型。語法表現爲:

type AnyString interface{ ~string }

此處的AnyString類型即可表示上述的 Phone | Email | Address,對於基礎類型爲string提供一個通用的脫敏函數 code 如下:

func Desensitization[T AnyString] (str T) string{
   var newStr string
   // Desensitization logic
 // newStr = desensitizationFunc(str)
 return newStr
}

聯合約束元素

聯合元素,寫成一系列由豎線 ( |) 分隔的約束元素。例如:int | float32~int8 | ~int16 | ~int32 | ~int64。並集元素的類型集是序列中每個元素的類型集的並集。聯合中列出的元素必須全部不同。這裏給所有有符號的數字類型添加一個通用的求和方法 coding 如下:

type SignedInteger interface {
   ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func SumOfSignedInteger[T SignedInteger](integers []SignedInteger "T SignedInteger") SignedInteger {
   sum := 0
   for i := range integers {
      sum += i
   }
   return sum
}

只能使用確定的類型進行聯合類型約束

 // GOOD
func PrintInt64OrFloat64[T int64|float64](t T "T int64|float64") {
   fmt.Printf( "%v\n" , t)
}

type someStruct struct {}

// GOOD
func PrintInt64OrSomeStruct[T int64|*someStruct](t T "T int64|*someStruct") {
   fmt.Printf( "t: %v\n" , t)
}

// BAD,不能在聯合類型中使用 ,且不能通過編譯
func handle[T io.Closer | Flusher](t T "T io.Closer | Flusher") {
   err := t.Flush()
   if err != nil {
      fmt.Println( "failed to flush: " , err.Error())
   }

   err = t.Close()
   if err != nil {
      fmt.Println( "failed to close: " , err.Error())
   }
}

type Flusher interface {
   Flush() error
}

約束中的可比類型

Go1.18 中內置了一個類型約束 comparable約束,comparable約束的類型集是所有可比較類型的集合。這允許使用該類型參數==!=值。

func Index[T comparable]([]T, x T "T comparable") int {
   for i, v := range s {
      if v == x {
         return i
      }
   }
   return -1
}

也可以comparable 內嵌到其他接口類型中使用:

type ComparableHasher interface {
   comparable
   Hash() uintptr
}

類型推斷

在許多情況下,可以使用類型推斷來避免必須顯式寫出部分或全部類型參數。可以對函數調用使用的參數類型推斷從非類型參數的類型中推斷出類型參數。開發者可以使用約束類型推斷從已知類型參數中推斷出未知類型參數。

在類型參數小節中調用Print函數時聲明瞭泛型函數調用的實際類型參數爲int,但因爲有類型推斷這一特性,開發者可以更加簡潔的使用泛型。例:

func Map[F, T any]([]F, f func("F, T any") T) []{ ... }

func E (){
   var s []int
   f := func(i int) int64 { return int64(i) }
   var r []int64
   // 標註兩個類型
r = Map[int, int64](s, f "int, int64")
   // 只指定第一個類型參數
r = Map[int](s, f "int")
   // 不指定任何類型參數,並讓兩者都被推斷。
r = Map(s, f)
}

Tips:

如果在沒有指定所有類型參數的情況下使用泛型函數或類型,則如果無法推斷出任何未指定的類型參數,則會出現錯誤。

(注意:類型推斷是一個方便的特性。雖然它是一個重要特性,但它並沒有給設計增加任何功能,只是方便使用它。在最初的實現中可以省略它,看看是否它似乎是必需的。也就是說,此功能不需要額外的語法,並且生成更具可讀性的代碼。)

泛型實踐

泛型函數式應用

Tips:

在函數式編程語言中,📖高階函數 [12] (Higher-order function)是一個重要的特性。高階函數是至少滿足下列一個條件的函數:

  • 接受一個或多個函數作爲輸入

  • 輸出一個函數

在 Go 中支持閉包的特性,所以很容易實現高階函數:

func foo(bar func() string) func() string {
   return  func() string {
      return  "foo" + " " + bar()
   }
}

func main() {
   bar := func() string {
      return  "bar"
}
   foobar := foo(bar)
   fmt.Println(foobar())
   // foo bar
}

Tips:

Go 有泛型這一特性,結合函數在 Go 中是一等公民,可以寫出一些常見的、類型間通用的函數,能應對一些類型組合操作,提高代碼的可讀性,增加可維護性。以下是幾個常用的高階函數:

filter 操作是高階函數的經典應用,它接受一個函數 f(func (T) bool)和一個線性表 l([]T),對 l 中的每個元素應用函數f,如結果爲 true,則將該元素加入新的線性表裏,否則丟棄該元素,最後返回新的線性表。(借用下 Java 中 Steam 的圖示,類似的道理)

而 Go 的泛型語法爲這種通用代碼提供了很好的抽象,可以很容易寫出一個簡單的filter函數

func Filter[T any](f func("T any") bool, src []T) []{
   var dst []T
   for _, v := range src {
      if f(v) {
         dst = append(dst, v)
      }
   }
   return dst
}

// 使用如下
func main() {
   src := []int{-2, -1, -0, 1, 2}
   // 過濾出大於等於0的元素
   dst := Filter(func(v int) bool { return v >= 0 }, src)
   fmt.Println(dst)
}
// Output:
// [0 1 2]

同爲高階函數的還有MapMap接受一個函數 f(func (T1) T2)和一個線性表 l1([]T1),對 l1 中的每個元素應用函數 f,返回的結果組成新的線性表 l2([]T2)。Map

一般長用於類型轉換或者屬性選擇,可用作常見 Obj 的轉換(DO、VO、PO、DTO 等):

func Map[S, T any](src []S, f func("S, T any") T) []{
   dst := make([]T, len(src))
   for i, v := range src {
      dst[i] = f(v)
   }
   return dst
}

func  main () { type User struct {
      Name string
      Age  int
   }
   users := []User{
      {Name: "John", Age: 20},
      {Name: "Mary", Age: 30},
      {Name: "Bob", Age: 40},
      {Name: "Alice", Age: 50},
      {Name: "Tom", Age: 60},
      {Name: "Jack", Age: 70},
   }
   // 屬性選擇
   names := Map(users, func(u User) string { return u.Name })
   fmt.Println(names)
   // [John Mary Bob Alice Tom Jack]
   ages := Map(users, func(u User) int { return u.Age })
   fmt.Println(ages)
   // [20 30 40 50 60 70]

    // 類型轉換
   ints := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
   strs := Map(ints, func(i int) string { return strconv.FormatInt(int64(i), 10) })
   fmt.Println(strs)
   // [1 2 3 4 5 6 7 8 9 10]
}

最後一個比較常見的是Reduce 接受一個函數f func(T, T) T和一個線性表 l1([]T1),將線性表中的每個元素執行函數f,並將先前元素的計算結果作爲參數傳入,最後將其結果彙總爲單個返回值:

func Reduce[T any](f func(T, T "T any") T, src []T) T {
   if len(src) == 1 {
      return src[0]
   }
   return f(src[0], Reduce(f, src[1:]))
}

func main() {
   ints := []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
   res := Reduce(func(a, b int64) int64 {
      return a + b
   }, ints)
   fmt.Println(res)
   // 55
}

指針方法

Tips:

在一些項目的實體定義中,某些實體可能帶有一些修改自身屬性的方法,這種方法帶有寫的語義,在 Go 中,這種方法會聲明接受者爲接收類型的指針。

那如何對這類實體應用泛型的相關特性呢?,這裏有一個簡單的小例子:

 // Setter 是一個類型約束
// 實現一個從字符串中設置值的 Set 方法。
type Setter interface {
   Set(string)
}

// FromStrings 接受一個字符串切片並返回一個 T 切片,
// 調用 Set 方法來設置每個返回值。
//
// 注意因爲 T 只用於結果參數,
// 調用時函數參數類型推斷不起作用
func FromStrings[T Setter]([]string "T Setter") []{
   result := make([]T, len(s))
   for i, v := range s {
      result[i].Set(v)
   }
   return result
}

構建一個調用的例子

 // 定義一個可設置的 int
type Settable int

// 從字符串中設置 *p 的值
func (p *Settable) Set(s string) {
   // 生產場景代碼不應該忽略錯誤
i, _ := strconv.Atoi(s)
   *p = Settable(i)
}

func F() {
   nums := FromStrings[Settable]([]string{ "1" , "2" })
}

這裏的目標是使用FromStrings函數獲得一個切片,但是此處會編譯錯誤,問題是FromStrings需要一個有Set(string)方法的類型。函數F試圖用轉換返回類型爲Settable,但Settable沒有Set方法。有Set方法的類型是*Settable,那在調用時將返回類型改變爲*Settable

func F() {
   nums := FromStrings[*Settable]([]string{ "1" , "2" })
}

當前可編譯,但是運行時會 panic,問題是FromStrings創建了一個 type 切片[]T。當用 實例化時*Settable,這意味着一個類型的切片[]*SettableFromStrings調用時result[i].Set(v),即調用Set存儲在result[i]. 那個指針是nil。該Settable.Set方法將由nil接收者調用,並由於nil取消引用錯誤而引發恐慌。

指針類型*Settable實現了約束,但代碼確實想使用非指針類型Settable。我們需要的是一種編寫方法FromStrings,它可以將類型Settable作爲參數但調用指針方法。重複一遍,我們不能使用Settable,因爲它沒有Set方法,我們不能使用*Settable,因爲不能創建 type 的切片Settable

這裏可以傳遞這個兩種類型實現如下:

type Setter2[B any] interface {
   Set(string)
   *B // non-interface type constraint element
}

func FromStrings2[T any, PT Setter2[T]]([]string "T any, PT Setter2[T]") []{
   result := make([]T, len(s))
   for i, v := range s {
      p := PT(&result[i])
      p.Set(v)
   }
   return result
}
func F() {
   nums := FromStrings2[Settable, *Settable]([]string{ "1" , "2" })
   // 現在 nums 是 []Settable{1, 2}。
   // 也可以使用類型推斷  會簡單點
   nums =  FromStrings2[Settable]([]string{"1""2"})
}

即可編譯,運行成功。

泛型零值

Tips:

Go 中現有泛型設計對於類型參數的零值並不好表達,Go 官方目前沒有更好的辦法,但是提供了一些目前可行的一些方案:

  • 對於目前泛型的設計:

  • 可用 var zero T,但是這裏需要額外去聲明下。

  • 使用*new(T)

  • 對於返回結果可命名結果參數,並使用裸return返回零值。

  • 擴展設計:

  • 設計以允許nil用作任何泛型類型的零值(但請參閱 issue 22729[13])。

  • 設計以允許使用T{}(其中T是類型參數)來指示類型的零值。

  • 更改語言以允許return ...返回結果類型的零值,如 issue 21182[14] 中所建議的那樣。

但目前來說一般使用 var zero T 的方式。

以下我們有一個隊列,使用泛型的chan實現,對這個結構體有些方法,最簡單的出隊入隊方法,但是對於泛型類型的變量,需要考慮零值的問題:

// 有一個對象,包含一個管道屬性,可以調用此對象方法壓入或彈出數據
type Queue[T any] struct {
   data chan T
}

// 構建新的隊列
func NewQueue[T any](size int "T any") Queue[T] {
   return Queue[T]{
      data: make(chan T, size),
   }
}

// 壓入數據
func (q Queue[T]) Push(val T) {
   q.data <- val
}

// 彈出數據 ,如果沒有數據會被阻塞
func (q Queue[T]) Pop() T {
   d := <-q.data
   return d
}

func (q Queue[T]) TryPop() (T, bool) {
   select {
   case val := <-q.data:
      return val, true
 default:
   // 編譯報錯
      return nil, false
}
}

// 在該代碼中,T可以是任何值,包括可能不爲nil的值。
// 我們可以利用var語句來解決這個問題,它生成一個新變量,並將其初始化爲該類型的零值:
func Zero[T any]( "T any") T {
  var zero T
  return zero
}

// 根據這一特性,可以改寫TryPop方法
func (q Queue[T]) TryPop() (T, bool) {
  select {
  case val := <-q.data:
    return val, true
  default:
  // 可編譯通過
    var zero T
    return zero, false
  }
}

總結

泛型是一個很大的語言特性,但目前在 Go 中還沒有太多的實踐,官方也沒有提供太多示例,但是可以通過加深對泛型中的基本元素的認知,瞭解其設計思想,結合編程範式、設計模式,相信會在工程實踐中真正的提高編碼效率。

參考文獻

參考資料

[1]

research.swtch.com/generic: https://research.swtch.com/generic

[2]

爲什麼 Go 語言沒有泛型 - 面向信仰編程: https://draveness.me/whys-the-design-go-generics/

[3]

Go.dev/blog/survey…: https://go.dev/blog/survey2020-results

[4]

github.com/Golang/Go/i…: https://github.com/golang/go/issues/43651

[5]

An Introduction To Generics: https://go.dev/blog/intro-generics

[6]

When To Use Generics: https://go.dev/blog/when-generics

[7]

Bounds: https://docs.oracle.com/javase/tutorial/java/generics/bounded.html

[8]

單態化: https://en.wikipedia.org/wiki/Monomorphization

[9]

泛型實現: https://github.com/golang/proposal/blob/master/design/generics-implementation-gcshape.md

[10]

GCShape stenciling with Dictionaries: https://go.googlesource.com/proposal/+/refs/heads/master/design/generics-implementation-gcshape.md

[11]

go.dev/blog/intro-…: https://go.dev/blog/intro-generics

[12]

📖高階函數: https://zh.wikipedia.org/wiki / 高階函數

[13]

issue 22729: https://golang.org/issue/22729

[14]

issue 21182: https://golang.org/issue/21182

[15]

《An Introduction To Generics》: https://go.dev/blog/intro-generics

[16]

《Type Parameters Proposal》: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md

[17]

《Generics implementation - GC Shape Stenciling》: https://github.com/golang/proposal/blob/master/design/generics-implementation-gcshape.md

轉自:

https://juejin.cn/post/7116817920209977351#heading-5

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oA5MVUg1dOS5iD_hE5V1Fw