教學課程:開始使用泛型

本教學課程介紹 Go 中泛型的基礎知識。使用泛型,您可以宣告和使用函式或類型,這些函式或類型撰寫成可與呼叫程式碼提供的任何一組類型搭配使用。

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

您將按部就班地完成下列各節

  1. 為你的程式碼建立一個資料夾。
  2. 加入非泛型函式。
  3. 加入一個泛型函式來處理多種型別。
  4. 呼叫泛型函式時移除型別引數。
  5. 宣告型別限制。

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

注意:如果你願意,你可以使用 「Go dev 分支」模式中的 Go playground 來編輯和執行你的程式。

先決條件

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

首先,為你將要撰寫的程式碼建立一個資料夾。

  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
    

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

接下來,你將加入一些簡單的程式碼來處理映射。

加入非泛型函式

在此步驟中,您將新增兩個函數,每個函數都會將地圖中的值加總並傳回總和。

您宣告兩個函數而不是一個函數,因為您使用兩種不同類型的映射:一種儲存 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
    }
    

    在此程式碼中,您

    • 宣告兩個函數,以將地圖中的值加總並傳回總和。
      • SumFloats 採用一個 string 對應到 float64 值的地圖。
      • SumInts 採用一個 string 對應到 int64 值的地圖。
  4. 在 main.go 最上方,在套件宣告下方,貼上以下 main 函數,以初始化兩個地圖,並在呼叫您在前一個步驟中宣告的函數時將它們用作引數。

    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 值的地圖和一個 int64 值的地圖,每個地圖都有兩個條目。
    • 呼叫您先前宣告的兩個函數,以找出每個地圖的值的總和。
    • 列印結果。
  5. 在 main.go 的最上方,就在套件宣告下方,匯入您需要用來支援您剛撰寫的程式碼的套件。

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

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

執行程式碼

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

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

使用泛型,你可以寫一個函式,而不是兩個。接下來,你將新增一個單一泛型函式,用於包含整數或浮點數值的映射。

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

在本節中,你將新增一個單一泛型函式,它可以接收包含整數或浮點數值的映射,有效地用一個函式取代你剛寫的兩個函式。

為了支援兩種類型的值,那個單一函式需要一種方式來宣告它支援的類型。另一方面,呼叫程式碼需要一種方式來指定它是否使用整數或浮點數映射進行呼叫。

為了支援這一點,你將撰寫一個函式,除了其一般函式參數之外,還宣告類型參數。這些類型參數使函式泛型化,使其能夠處理不同類型的引數。你將使用類型引數和一般函式引數來呼叫函式。

每個類型參數都有一個類型約束,它作為類型參數的一種元類型。每個類型約束指定呼叫程式碼可對應類型參數使用的允許類型引數。

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

請記住,類型參數必須支援泛型程式碼對其執行的所有作業。例如,如果函式的程式碼嘗試對包含數字類型的約束的類型參數執行字串作業(例如索引),則程式碼無法編譯。

在您即將撰寫的程式碼中,您將使用允許整數或浮點數類型的約束。

撰寫程式碼

  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` 函式,`K` 和 `V`,以及一個使用類型參數的引數,類型為 `map[K]V` 的 `m`。函式傳回類型為 `V` 的值。
    • 為 `K` 類型參數指定類型約束 `comparable`。`comparable` 約束是專門針對此類情況預先宣告在 Go 中。它允許任何類型,其值可用作比較運算子 `==` 和 `!=` 的運算元。Go 要求映射鍵必須可比較。因此,將 `K` 宣告為 `comparable` 是必要的,這樣您才能在映射變數中使用 `K` 作為鍵。它還確保呼叫程式碼對映射鍵使用允許的類型。
    • 為 `V` 類型參數指定一個約束,該約束是兩個類型的聯集:`int64` 和 `float64`。使用 `|` 指定兩個類型的聯集,表示此約束允許任一類型。編譯器將允許任一類型作為呼叫程式碼中的引數。
    • 指定 m 參數的類型為 map[K]V,其中 KV 是已為類型參數指定的類型。請注意,我們知道 map[K]V 是有效的映射類型,因為 K 是可比較的類型。如果我們沒有宣告 K 可比較,編譯器將拒絕引用 map[K]V
  2. 在 main.go 中,貼上以下程式碼到您已有的程式碼下方。

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

    在此程式碼中,您

    • 呼叫您剛宣告的泛型函式,傳遞您建立的每個映射。

    • 指定類型參數,也就是方括弧中的類型名稱,以明確您要呼叫的函式中應取代類型參數的類型。

      如您在下一節中所見,您通常可以在函式呼叫中省略類型參數。Go 通常可以從您的程式碼中推斷它們。

    • 列印函式傳回的總和。

執行程式碼

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

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

若要執行您的程式碼,編譯器會在每個呼叫中將類型參數替換為該呼叫中指定的具體類型。

在呼叫您撰寫的泛型函式時,您指定了類型參數,告訴編譯器在函式的類型參數中使用哪些類型。如您在下一節中所見,在許多情況下,您可以省略這些類型參數,因為編譯器可以推斷它們。

在呼叫泛型函式時移除類型參數

在本節中,您將新增泛型函式呼叫的修改版本,對呼叫程式碼進行小幅變更以簡化。您將移除類型參數,因為在這種情況下不需要它們。

當 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

接下來,你將進一步簡化函數,將整數和浮點數的聯集擷取到類型約束中,以便你可以重複使用,例如從其他程式碼中。

宣告類型約束

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

你將類型約束宣告為介面。約束允許任何實作介面的類型。例如,如果你宣告一個具有三個方法的類型約束介面,然後在泛型函數中將它與類型參數一起使用,則用於呼叫函數的類型參數必須具備所有這些方法。

約束介面也可以參照特定類型,如你將在本節中看到的。

撰寫程式碼

  1. 就在 main 的上方,在匯入陳述式的正下方,貼上以下程式碼以宣告類型約束。

    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))
    

    在此程式碼中,您

    • 使用每個映射呼叫 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 中的泛型。

建議接下來的主題

已完成的程式碼

您可以在 Go playground 中執行此程式。在 playground 中,只需按一下執行按鈕即可。

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
}