• English
  • 泛型入門

    本教學介紹了 Go 中泛型的基礎知識。使用泛型,你可以宣告和使用為呼叫程式碼的一組型別中的任何型別工作的函數或型別。

    在本教學中,你將宣告兩個簡單的非泛型函數,然後在單個泛型函數中捕獲相同的邏輯。

    你將經歷以下部分:

    1. 為程式碼建立一個資料夾。
    2. 加入非泛型函數。
    3. 加入一個泛型函數來處理多種型別。
    4. 呼叫泛型函數時刪除型別參數。
    5. 宣告型別約束。

    注意: 如果你願意,可以使用 Go playground 的 “Go dev branch” 模式 來編輯和執行你的程式。

    先決條件

    • 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

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

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

    加入非泛型函數

    在此步驟中,你將加入兩個函數,每個函數將映射的值加在一起並返回總和。

    你宣告兩個函數而不是一個,因為你正在處理兩種不同型別的映射:一個儲存 int64 值,另一個儲存 float64 值。

    撰寫程式碼

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

    2. 將以下套件宣告貼上到 main.go 的頂部。

      package main

      一個獨立的程式(與函式庫相對)始終在 main 套件中。

    3. 在套件宣告下方,貼上以下兩個函數宣告。

      // SumInts 將 m 的值加在一起。
      func SumInts(m map[string]int64) int64 {
      	var s int64
      	for _, v := range m {
      		s += v
      	}
      	return s
      }
      
      // SumFloats 將 m 的值加在一起。
      func SumFloats(m map[string]float64) float64 {
      	var s float64
      	for _, v := range m {
      		s += v
      	}
      	return s
      }

      在這段程式碼中,你:

      • 宣告兩個函數來將映射的值加在一起並返回總和。
        • SumFloats 接受一個 stringfloat64 值的映射。
        • SumInts 接受一個 stringint64 值的映射。
    4. main.go 的頂部,套件宣告下方,貼上以下 main 函數以初始化兩個映射,並在呼叫你在上一步中宣告的函數時使用它們作為參數。

      func main() {
      	// 初始化整數映射
      	ints := map[string]int64{
      		"first":  34,
      		"second": 12,
      	}
      
      	// 初始化浮點數映射
      	floats := map[string]float64{
      		"first":  35.98,
      		"second": 26.99,
      	}
      
      	fmt.Printf("非泛型求和: %v 和 %v\n",
      		SumInts(ints),
      		SumFloats(floats))
      }

      在這段程式碼中,你:

      • 初始化一個 float64 值的映射和一個 int64 值的映射,每個映射有兩個條目。
      • 呼叫你之前宣告的兩個函數來找到每個映射值的總和。
      • 列印結果。
    5. main.go 的頂部,套件宣告下方,匯入你剛撰寫程式碼所需的套件。

      程式碼的前幾行應如下所示:

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

    執行程式碼

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

    $ go run .
    非泛型求和: 46 和 62.97

    使用泛型,你可以在這裡寫一個函數而不是兩個。接下來,你將加入一個適用於包含整數或浮點值的映射的單個泛型函數。

    加入一個泛型函數來處理多種型別

    在本節中,你將加入一個單一的泛型函數,可以接收包含整數或浮點值的映射,有效地用單個函數替換你剛剛撰寫的兩個函數。

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

    為此,你將撰寫一個函數,該函數宣告型別參數以及普通函數參數。這些型別參數使函數泛型,使其能夠處理不同型別的參數。你將使用型別參數和普通函數參數呼叫該函數。

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

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

    請記住,型別參數必須支援泛型程式碼在其上執行的所有操作。例如,如果你的函數程式碼嘗試對包含數值型別的型別參數執行 string 操作(如索引),程式碼將無法編譯。

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

    撰寫程式碼

    1. 在你之前加入的兩個函數下方,貼上以下泛型函數。

      // SumIntsOrFloats 對映射 m 的值求和。它支援 map 值的型別 int64 和 float64。
      func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
          var s V
          for _, v := range m {
              s += v
          }
          return s
      }

      在這段程式碼中,你:

      • 宣告一個帶有兩個型別參數(在方括號內)KVSumIntsOrFloats 函數,以及一個使用型別參數的參數 m,型別為 map[K]V。該函數返回一個型別為 V 的值。
      • K 型別參數指定 comparable 型別約束。comparable 約束是 Go 中預先宣告的,專門用於此類情況。它允許任何型別的值用作比較運算子 ==!= 的操作數。Go 要求映射鍵是可比較的。因此,將 K 宣告為 comparable 是必要的,以便你可以在映射變數中使用 K 作為鍵。它還確保呼叫程式碼使用允許的映射鍵型別。
      • V 型別參數指定一個約束,即兩種型別的聯合:int64float64。使用 | 指定兩種型別的聯合,這意味著此約束允許任一種型別。編譯器將允許任一種型別作為呼叫程式碼中的參數。
      • 指定 m 參數的型別為 map[K]V,其中 KV 是為型別參數指定的型別。注意,我們知道 map[K]V 是一個有效的映射型別,因為 K 是一個可比較的型別。如果我們沒有宣告 Kcomparable,編譯器將拒絕引用 map[K]V
    2. main.go 中,在你已有的程式碼下方,貼上以下程式碼。

      fmt.Printf("泛型求和: %v 和 %v\n",
      	SumIntsOrFloats[string, int64](ints),
      	SumIntsOrFloats[string, float64](floats))

      在這段程式碼中,你:

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

      • 指定型別參數 —— 方括號中的型別名稱 —— 以明確你呼叫的函數中應替換型別參數的型別。

        正如你將在下一節中看到的,你通常可以省略這些型別參數,因為 Go 通常可以從你的程式碼中推斷它們。

      • 列印函數返回的總和。

    執行程式碼

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97

    為了執行你的程式碼,在每次呼叫中,編譯器將型別參數替換為在該呼叫中指定的具體型別。

    在你撰寫的泛型函數呼叫中,你指定了型別參數,告訴編譯器在函數的泛型參數位置使用什麼型別。正如你將在下一節中看到的,在許多情況下,你可以省略這些型別參數,因為編譯器可以推斷它們。

    呼叫泛型函數時刪除型別參數

    在本節中,你將加入一個泛型函數呼叫的修改版本,透過省略型別參數來簡化呼叫程式碼。在這種情況下,型別參數不是必需的。

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

    請注意,這並不總是可能的。例如,如果你需要呼叫一個沒有參數的泛型函數,你需要在函數呼叫中包含型別參數。

    撰寫程式碼

    • main.go 中,在你已有的程式碼下方,貼上以下程式碼。

      fmt.Printf("泛型求和,型別參數推斷: %v 和 %v\n",
      	SumIntsOrFloats(ints),
      	SumIntsOrFloats(floats))

      在這段程式碼中,你:

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

    執行程式碼

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97
    泛型求和,型別參數推斷: 46 和 62.97

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

    宣告型別約束

    在最後一節中,你將把之前定義的約束移動到自己的介面中,以便在多個地方重用。以這種方式宣告約束有助於簡化程式碼,特別是當約束更複雜時。

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

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

    撰寫程式碼

    1. main 上方,緊接在匯入語句之後,貼上以下程式碼以宣告型別約束。

      type Number interface {
          int64 | float64
      }

      在這段程式碼中,你:

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

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

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

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

      // SumNumbers 對映射 m 的值求和。它支援整數和浮點數作為映射值。
      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("使用約束的泛型求和: %v 和 %v\n",
      	SumNumbers(ints),
      	SumNumbers(floats))

      在這段程式碼中,你:

      • 使用每個映射呼叫 SumNumbers,列印每個值的總和。

        與前一節一樣,你在對泛型函數的呼叫中省略了型別參數(方括號中的型別名稱)。Go 編譯器可以從其他參數中推斷型別參數。

    執行程式碼

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

    $ go run .
    非泛型求和: 46 和 62.97
    泛型求和: 46 和 62.97
    泛型求和,型別參數推斷: 46 和 62.97
    使用約束的泛型求和: 46 和 62.97

    結論

    幹得好!你剛剛介紹了 Go 中的泛型。

    建議的後續主題:

    完整程式碼

    你可以在 Go playground 中執行此程式。在 playground 上,只需點擊 執行 按鈕。

    package main
    
    import "fmt"
    
    type Number interface {
    	int64 | float64
    }
    
    func main() {
    	// 初始化整數映射
    	ints := map[string]int64{
    		"first": 34,
    		"second": 12,
    	}
    
    	// 初始化浮點數映射
    	floats := map[string]float64{
    		"first": 35.98,
    		"second": 26.99,
    	}
    
    	fmt.Printf("非泛型求和: %v 和 %v\n",
    		SumInts(ints),
    		SumFloats(floats))
    
    	fmt.Printf("泛型求和: %v 和 %v\n",
    		SumIntsOrFloats[string, int64](ints),
    		SumIntsOrFloats[string, float64](floats))
    
    	fmt.Printf("泛型求和,型別參數推斷: %v 和 %v\n",
    		SumIntsOrFloats(ints),
    		SumIntsOrFloats(floats))
    
    	fmt.Printf("使用約束的泛型求和: %v 和 %v\n",
    		SumNumbers(ints),
    		SumNumbers(floats))
    }
    
    // SumInts 將 m 的值加在一起。
    func SumInts(m map[string]int64) int64 {
    	var s int64
    	for _, v := range m {
    		s += v
    	}
    	return s
    }
    
    // SumFloats 將 m 的值加在一起。
    func SumFloats(m map[string]float64) float64 {
    	var s float64
    	for _, v := range m {
    		s += v
    	}
    	return s
    }
    
    // SumIntsOrFloats 對映射 m 的值求和。它支援浮點數和整數作為映射值。
    func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    	var s V
    	for _, v := range m {
    		s += v
    	}
    	return s
    }
    
    // SumNumbers 對映射 m 的值求和。它支援整數和浮點數作為映射值。
    func SumNumbers[K comparable, V Number](m map[K]V) V {
    	var s V
    	for _, v := range m {
    		s += v
    	}
    	return s
    }