Go 官方出品泛型教程:如何開始使用泛型

nono_94發表於2021-12-19

目錄

備註:這是一個 beta 版本的內容

這個教程介紹了 Go 泛型的基礎概念。 通過泛型,你可以宣告並使用函式或者是型別,那些用於呼叫程式碼時引數需要相容多個不同型別的情況。

在這個教程裡,你會宣告兩個普通的函式,然後複製一份相同的邏輯到一個泛型的方法裡。

你會通過以下幾個章節來進行學習:

  1. 為你的程式碼建立一個資料夾;
  2. 新增非泛型函式;
  3. 新增一個泛型函式來處理多種型別;
  4. 在呼叫泛型函式時刪除型別引數;
  5. 宣告一個型別約束。

備註:對其他教程,可以檢視教程

備註:你同時也可以使用 "Go dev branch"模式來編輯和執行你的程式碼,如果你更喜歡以這種形式的話

前提條件

  • 安裝 Go 1.18 Beta 1 或者更新的版本。對安裝流程,請看安裝並使用 beta 版本。
  • 程式碼編輯器。任何你順手的程式碼編輯器。
  • 一個命令終端。Go 在任何終端工具上都很好使用,比如 Linux 、Mac、PowerShell 或者 Windows 上的 cmd。

安裝並使用 beta 版本

這個教程需要 Beta 1 的泛型特性。安裝 beta 版本,需要通過下面幾個步驟:

1、 執行下面的指令安裝 beta 版本

$ go install golang.org/dl/go1.18beta1@latest

2、 執行下面的指令下載更新

$ go1.18beta1 download

3、用 beta 版本執行 go 命令,而不是 Go 的釋出版本 (如果你本地有安裝的話)

你可以使用 beta 版本名稱或者把 beta 重新命名成別的名稱來執行命令。

  • 使用 beta 版本名稱,你可以通過 go1.18beta1 來執行指令而不是 go:

    $ go1.18beta1 version
    
  • 通過對 beta 版本名稱重新命名,你可以簡化指令:

    $ alias go=go1.18beta1
    $ go version
    

在這個教程中將假設你已經對 beta 版本名稱進行了重新命名。

為你的程式碼建立一個資料夾

在一開始,先給你要寫的程式碼建立一個資料夾

1、 開啟一個命令提示符並切換到/home 資料夾

在 Linux 或者 Mac 上:

$ cd

在 windows 上:

C:\> cd %HOMEPATH%

在接下去的教程裡面會用$來代表提示符。指令在 windows 上也適用。

2、 在命令提示符下,為你的程式碼建立一個名為 generics 的目錄

$ mkdir generics
$ cd generics

3、 建立一個 module 來存放你的程式碼

執行go mod init指令,引數為你新程式碼的 module 路徑

$ go mod init example/generics
go: creating new go.mod: module example/generics

備註:對生產環境,你會指定一個更符合你自己需求的 module 路徑。更多的請看依賴管理

接下來,你會增加一些簡單的和 maps 相關的程式碼。

新增普通函式

在這一步中,你將新增兩個函式,每個函式都會累加 map 中的值 ,並返回總和。

你將宣告兩個函式而不是一個,因為你要處理兩種不同型別的 map:一個儲存 int64 型別的值,另一個儲存 float64 型別的值。

寫程式碼

1、 用你的文字編輯器,在 generics 資料夾裡面建立一個叫 main.go 的檔案。你將會在這個檔案內寫你的 Go 程式碼。

2、 到 main.go 檔案的上方,貼上如下的包的宣告。

package main

一個獨立的程式(相對於一個庫)總是在 main 包中。

3、 在包的宣告下面,貼上以下兩個函式的宣告。

// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

在這段程式碼中,你:

  • 宣告兩個函式,將一個 map 的值加在一起,並返回總和。
    • SumFloats 接收一個 map,key 為 string 型別,value 為 floa64 型別。
    • SumInt 接收一個 map,key 為 string 型別,value 為 int64 型別。

4、 在 main.go 的頂部,包宣告的下面,貼上以下 main 函式,用來初始化兩個 map,並在呼叫你在上一步中宣告的函式時將它們作為引數。

func main() {
// Initialize a map for the integer values
ints := map[string]int64{
    "first": 34,
    "second": 12,
}

// Initialize a map for the float values
floats := map[string]float64{
    "first": 35.98,
    "second": 26.99,
}

fmt.Printf("Non-Generic Sums: %v and %v\n",
    SumInts(ints),
    SumFloats(floats))
}

在這段程式碼中,你:

  • 初始化一個 key 為 string,value 為float64的 map 和一個 key 為 string,value 為int64的 map,各有 2 條資料;
  • 呼叫之前宣告的兩個方法來獲取每個 map 的值的總和;
  • 列印結果。

5、 靠近 main.go 頂部,僅在包宣告的下方,匯入你剛剛寫的程式碼所需要引用的包。

第一行程式碼應該看起來如下所示:

package main
import "fmt"

6、 儲存 main.go.

執行程式碼

在 main.go 所在目錄下,通過命令列執行程式碼

$ go run .
Non-Generic Sums: 46 and 62.97

有了泛型,你可以只寫一個函式而不是兩個。接下來,你將為 maps 新增一個泛型函式,來允許接收整數型別或者是浮點數型別。

新增泛型函式處理多種型別

在這一節,你將會新增一個泛型函式來接收一個 map,可能值是整數型別或者浮點數型別的 map,有效地用一個函式替換掉你剛才寫的 2 個函式。

為了支援不同型別的值,這個函式需要有一個方法來宣告它所支援的型別。另一方面,呼叫程式碼將需要一種方法來指定它是用整數還是浮點數來呼叫。

為了實現上面的描述,你將會宣告一個除了有普通函式引數,還有型別引數的函式。這個型別引數實現了函式的通用性,使得它可以處理多個不同的型別。你將會用型別引數和普通函式引數來呼叫這個泛型函式。

每個型別引數都有一個型別約束,類似於每個型別引數的 meta-type。每個型別約束都指定了呼叫程式碼時每個對應輸入引數的可允許的型別。

雖然型別引數的約束通常代表某些型別,但是在編譯的時候型別引數只代表一個型別 - 在呼叫程式碼時作為型別引數。如果型別引數的型別不被型別引數的約束所允許,程式碼則無法編譯。

需要記住的是型別引數必須滿足泛型程式碼對它的所有的操作。舉個例子,如果你的程式碼嘗試去做一些 string 的操作 (比如索引),而這個型別引數包含數字的型別,那程式碼是無法編譯的。

在下面你要編寫的程式碼裡,你會使用允許整數或者浮點數型別的限制。

寫程式碼

1、 在你之前寫的兩個函式的下方,貼上下面的泛型函式

// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

在這段程式碼裡,你:

  • 宣告瞭一個帶有 2 個型別引數 (方括號內) 的 SumIntsOrFloats 函式,K 和 V,一個使用型別引數的引數,型別為 map[K] V 的引數 m。
    • 為 K 型別引數指定可比較的型別約束。事實上,針對此類情況,在 Go 裡面可比較的限制是會提前宣告。它允許任何型別的值可以作為比較運算子==和!=的操作符。在 Go 裡面,map 的 key 是需要可比較的。因此,將 K 宣告為可比較的是很有必要的,這樣你就可以使用 K 作為 map 變數的 key。這樣也保證了呼叫程式碼方使用一個被允許的型別做 map 的 key。
    • 為 V 型別引數指定一個兩個型別合集的型別約束:int64 和 float64。使用|指定了 2 個型別的合集,表示約束允許這兩種型別。任何一個型別都會被編譯器認定為合法的傳參引數。
    • 指定引數 m 為型別 map[K] V,其中 K 和 V 的型別已經指定為型別引數。注意到因為 K 是可比較的型別,所以 map[K] V 是一個有效的 map 型別。如果我們沒有宣告 K 是可比較的,那麼編譯器會拒絕對 map[K] V 的引用。

2、 在 main.go 裡,在你現在的程式碼下方,貼上如下程式碼:

fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

在這段程式碼裡,你:

  • 呼叫你剛才宣告的泛型函式,傳遞你建立的每個 map。

  • 指定型別引數 - 在方括號內的型別名稱 - 來明確你所呼叫的函式中應該用哪些型別來替代型別引數。

    你將會在下一節看到,你通常可以在函式呼叫時省略型別引數。Go 通常可以從程式碼裡推斷出來。

  • 列印函式返回的總和。

執行程式碼

在 main.go 所在目錄下,通過命令列執行程式碼

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

為了執行你的程式碼,在每次呼叫的時候,編譯器都會用該呼叫中指定的具體型別替換型別引數。

在呼叫你寫的泛型函式時,你指定了型別引數來告訴編譯器用什麼型別來替換函式的型別引數。正如你將在下一節所看到的,在許多情況下,你可以省略這些型別引數,因為編譯器可以推斷出它們。

當呼叫泛型函式時移除型別引數

在這一節,你會新增一個泛型函式呼叫的修改版本,通過一個小的改變來簡化程式碼。在這個例子裡你將移除掉不需要的型別引數。

當 Go 編譯器可以推斷出你要使用的型別時,你可以在呼叫程式碼中省略型別引數。編譯器從函式引數的型別中推斷出型別引數。

注意這不是每次都可行的。舉個例子,如果你需要呼叫一個沒有引數的泛型函式,那麼你需要在呼叫函式時帶上型別引數。

寫程式碼

  • 在 main.go 的程式碼下方,貼上下面的程式碼。
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

在這段程式碼裡,你:

  • 呼叫泛型函式,省略型別引數。

執行程式碼

在 main.go 所在目錄下,通過命令列執行程式碼

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97

接下來,你將通過把整數和浮點數的合集定義到一個你可以重複使用的型別約束中,比如從其他的程式碼,來進一步簡化這個函式。

宣告型別約束

在最後一節中,你將把你先前定義的約束移到它自己的 interface 中,這樣你就可以在多個地方重複使用它。以這種方式宣告約束有助於簡化程式碼,尤其當一個約束越來越複雜的時候。

你將型別引數定義為一個 interface。約束允許任何型別實現這個 interface。舉個例子,如果你定義了一個有三個方法的型別引數 interface,然後用它作為一個泛型函式的型別引數,那麼呼叫這個函式的型別引數必須實現這些方法。

你將在本節中看到,約束 interface 也可以指代特定的型別。

寫程式碼

1、 在 main 函式上面,緊接著 import 下方,貼上如下程式碼來定義型別約束。

type Number interface {
    int64 | float64
}

在這段程式碼裡,你:

  • 宣告一個 Number interface 型別作為型別限制

  • 在 interface 內宣告 int64 和 float64 的合集

    本質上,你是在把函式宣告中的合集移到一個新的型別約束中。這樣子,當你想要約束一個型別引數為 int64 或者 float64,你可以使用 Number interface 而不是寫 int64 | float64。

2、 在你已寫好的函式下方,貼上如下泛型函式,SumNumbers。

// SumNumbers sums the values of map m. Its supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

在這段程式碼,你:

  • 宣告一個泛型函式,其邏輯與你之前宣告的泛型函式相同,但是是使用新的 interface 型別作為型別引數而不是合集。和之前一樣,你使用型別引數作為引數和返回型別。

3、 在 main.go,在你已寫完的程式碼下方,貼上如下程式碼。

fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

在這段程式碼裡,你:

  • 每個 map 依次呼叫 SumNumbers,並列印數值的總和。
  • 與上一節一樣,你可以在呼叫泛型函式時省略型別引數(方括號中的型別名稱)。Go 編譯器可以從其他引數中推斷出型別引數。

執行程式碼

在 main.go 所在目錄下,通過命令列執行程式碼

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

總結

完美結束!你剛才已經給你自己介紹了 Go 的泛型。

如果你想繼續試驗,你可以嘗試用整數約束和浮點數約束來寫 Number interface,來允許更多的數字型別。

建議閱讀的相關文章:

完整程式碼

你可以在 Go playground 執行這個程式碼。在 playground 只需要點選Run按鈕即可。

package main

import "fmt"

type Number interface {
    int64 | float64
}

func main() {
    // Initialize a map for the integer values
    ints := map[string]int64{
        "first": 34,
        "second": 12,
    }

    // Initialize a map for the float values
    floats := map[string]float64{
        "first": 35.98,
        "second": 26.99,
    }

    fmt.Printf("Non-Generic Sums: %v and %v\n",
        SumInts(ints),
        SumFloats(floats))

    fmt.Printf("Generic Sums: %v and %v\n",
        SumIntsOrFloats[string, int64](ints),
        SumIntsOrFloats[string, float64](floats))

    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
        SumIntsOrFloats(ints),
        SumIntsOrFloats(floats))

    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
        SumNumbers(ints),
        SumNumbers(floats))
}

// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

// SumIntsOrFloats sums the values of map m. It supports both floats and integers
// as map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

// SumNumbers sums the values of map m. Its supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}
更多原創文章乾貨分享,請關注公眾號
  • Go 官方出品泛型教程:如何開始使用泛型
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章