教程:泛型入門

本教程將介紹 Go 中泛型的基礎知識。使用泛型,您可以宣告和使用為與呼叫程式碼提供的任何一組型別協同工作而編寫的函式或型別。

在本教程中,您將宣告兩個簡單的非泛型函式,然後將相同的邏輯捕獲到一個泛型函式中。

您將按以下章節進行學習:

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

注意: 有關其他教程,請參閱教程

注意: 如果您願意,可以使用 “Go 開發分支”模式的 Go playground 來編輯和執行您的程式。

先決條件

  • 安裝 Go 1.18 或更高版本。 有關安裝說明,請參閱 安裝 Go
  • 一個程式碼編輯工具。 任何文字編輯器都可以。
  • 命令終端。 Go 在 Linux 和 Mac 上的任何終端以及 Windows 上的 PowerShell 或 cmd 中都能很好地工作。

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

首先,為您的程式碼建立一個資料夾。

  1. 開啟命令提示符並切換到您的主目錄。

    在 Linux 或 Mac 上

    $ cd
    

    在 Windows 上

    C:\> cd %HOMEPATH%
    

    本教程的其餘部分將顯示 $ 作為提示符。您使用的命令在 Windows 上也適用。

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

    $ mkdir generics
    $ cd generics
    
  3. 建立一個模組來存放您的程式碼。

    執行 go mod init 命令,並提供新程式碼的模組路徑。

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

    注意: 對於生產程式碼,您會指定一個更符合您自身需求的模組路徑。有關更多資訊,請參閱 管理依賴項

接下來,您將新增一些簡單的程式碼來處理 map。

新增非泛型函式

在此步驟中,您將新增兩個函式,每個函式都將 map 的值相加並返回總和。

您聲明瞭兩個函式而不是一個,因為您處理的是兩種不同型別的 map:一種儲存 int64 值,另一種儲存 float64 值。

編寫程式碼

  1. 使用您的文字編輯器,在 generics 目錄中建立一個名為 main.go 的檔案。您將在該檔案中編寫 Go 程式碼。

  2. 在 main.go 檔案的頂部,貼上以下包宣告。

    package main
    

    獨立程式(與庫相對)始終位於 `main` 包中。

  3. 在 package 宣告下方,貼上以下兩個函式宣告。

    // 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 接收一個從 stringfloat64 值的 map。
      • SumInts 接收一個從 stringint64 值的 map。
  4. 在 main.go 的頂部,package 宣告下方,貼上以下 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))
    }
    

    在此程式碼中,您

    • 初始化一個 float64 值 map 和一個 int64 值 map,每個 map 包含兩個條目。
    • 呼叫您之前宣告的兩個函式來查詢每個 map 值的總和。
    • 列印結果。
  5. 在 main.go 的頂部附近,package 宣告正下方,匯入您需要支援您剛剛編寫的程式碼的包。

    程式碼的第一行應如下所示:

    package main
    
    import "fmt"
    
  6. 儲存 main.go。

執行程式碼

在包含 main.go 的目錄的命令列中,執行程式碼。

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

使用泛型,您可以編寫一個函式而不是兩個。接下來,您將新增一個單一的泛型函式來處理包含整數或浮點數值的 map。

新增一個泛型函式來處理多種型別

在本節中,您將新增一個單一的泛型函式,該函式可以接收包含整數或浮點數值的 map,從而有效地用一個函式替換您剛寫的兩個函式。

為了支援這兩種型別的值,該單一函式需要一種方法來宣告它支援的型別。另一方面,呼叫程式碼需要一種方法來指定它是使用整數 map 還是浮點 map 進行呼叫。

為了支援這一點,您將編寫一個函式,該函式除了普通的函式引數外,還宣告型別引數。這些型別引數使函式成為泛型函式,使其能夠使用不同型別的引數。您將使用型別引數和普通函式引數來呼叫該函式。

每個型別引數都有一個型別約束,它充當該型別引數的一種元型別。每個型別約束都指定呼叫程式碼可以為相應型別引數使用的允許的型別引數。

雖然型別引數的約束通常代表一組型別,但在編譯時,型別引數代表單個型別——呼叫程式碼作為型別引數提供的型別。如果型別引數的型別不被型別引數的約束允許,程式碼將無法編譯。

請記住,型別引數必須支援泛型程式碼正在對其執行的所有操作。例如,如果您的函式程式碼嘗試對包含數字型別的型別引數執行 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
    }
    

    在此程式碼中,您

    • 宣告一個 SumIntsOrFloats 函式,它有兩個型別引數(在方括號內),KV,以及一個使用型別引數的引數,m,型別為 map[K]V。該函式返回一個 V 型別的 V alue。
    • K 型別引數指定 comparable 型別約束。comparable 約束專為此類情況設計,它在 Go 中是預先宣告的。它允許任何其值可以用作比較運算子 ==!= 的運算元的型別。Go 要求 map 的鍵必須是可比較的。因此,將 K 宣告為 comparable 是必要的,以便您可以使用 K 作為 map 變數的鍵。它還確保呼叫程式碼使用允許的型別作為 map 鍵。
    • V 型別引數指定一個由兩種型別組成的聯合約束:int64float64。使用 | 指定兩種型別的聯合,這意味著該約束允許這兩種型別中的任何一種。編譯器將允許這兩種型別中的任何一種作為呼叫程式碼中的引數。
    • 指定 m 引數的型別為 map[K]V,其中 KV 是已為型別引數指定的型別。請注意,我們知道 map[K]V 是一個有效的 map 型別,因為 K 是一個可比較的型別。如果我們沒有宣告 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

接下來,您將透過將整數和浮點數的聯合捕獲到一個可重用的型別約束中(例如,從其他程式碼中)來進一步簡化該函式。

宣告型別約束

在最後一個部分,您將把之前定義的約束移到它自己的介面中,以便您可以在多個地方重用它。以這種方式宣告約束有助於簡化程式碼,例如當約束更復雜時。

您將型別約束宣告為一個介面。該約束允許實現該介面的任何型別。例如,如果您聲明瞭一個帶有三個方法的型別約束介面,然後將其與泛型函式中的型別引數一起使用,則用於呼叫函式的型別引數必須具有所有這些方法。

約束介面也可以引用特定型別,正如您在本節中將看到的。

編寫程式碼

  1. main 函式正上方,緊跟在 import 語句之後,貼上以下程式碼來宣告一個型別約束。

    type Number interface {
        int64 | float64
    }
    

    在此程式碼中,您

    • 宣告 Number 介面型別以用作型別約束。

    • 在介面內宣告 int64float64 的聯合。

      本質上,您正在將聯合從函式宣告移到一個新的型別約束中。這樣,當您想將型別引數約束為 int64float64 時,您可以使用這個 Number 型別約束,而不是寫出 int64 | float64

  2. 在您已有的函式下方,貼上以下泛型 SumNumbers 函式。

    // SumNumbers sums the values of map m. It 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
    }
    

    在此程式碼中,您

    • 宣告一個與您之前宣告的泛型函式具有相同邏輯的泛型函式,但使用新的介面型別而不是聯合作為型別約束。與之前一樣,您使用型別引數作為引數和返回型別。
  3. 在 main.go 中,在您已有的程式碼下方,貼上以下程式碼。

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

    在此程式碼中,您

    • 用每個 map 呼叫 SumNumbers,並列印每個 map 中值的總和。

      與上一節一樣,您在呼叫泛型函式時省略了型別引數(方括號中的型別名稱)。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 中接觸了泛型。

建議的後續主題

完成的程式碼

您可以在 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
}