golang 1.18 泛型教程

原文:https://makeoptim.com/golang/generics-tutorial

前言

本教程介紹 Go 中泛型的基礎(chǔ)知識。使用泛型,你可以聲明和使用編寫為與調(diào)用代碼提供的任何一組類型一起使用的函數(shù)或類型。

在本教程中,你將聲明兩個(gè)簡單的非泛型函數(shù),然后在單個(gè)泛型函數(shù)中實(shí)現(xiàn)相同的邏輯。

你將逐步完成以下部分:

  • 為你的代碼創(chuàng)建一個(gè)文件夾。
  • 添加非泛型函數(shù)。
  • 添加一個(gè)通用函數(shù)來處理多種類型。
  • 調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù)。
  • 聲明類型約束。

注意:有關(guān)其他教程,請參閱教程

注意:如果你愿意,可以使用 “Go dev 分支”模式下的 Go Playground 來編輯和運(yùn)行你的程序。

先決條件

  • Go 1.18 或更高版本的安裝。有關(guān)安裝說明,請參閱安裝 Go
  • 用于編輯代碼的工具。你擁有的任何文本編輯器都可以正常工作。
  • 一個(gè)命令終端。Go 在 Linux 和 Mac 上的任何終端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。

為你的代碼創(chuàng)建一個(gè)文件夾

首先,為你要編寫的代碼創(chuàng)建一個(gè)文件夾。

  1. 打開命令提示符并切換到你的主目錄。

在 Linux 或 Mac 上:

$ cd

在 Windows 上:

C:\> cd %HOMEPATH%

本教程的其余部分將顯示 $ 作為提示。你使用的命令也可以在 Windows 上運(yùn)行。

  1. 在命令提示符下,為你的代碼創(chuàng)建一個(gè)名為 generics 的目錄。
$ mkdir generics
$ cd generics
  1. 創(chuàng)建一個(gè)模塊來保存你的代碼。

運(yùn)行 go mod init 命令,為其提供新代碼的模塊路徑。

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

注意:對于生產(chǎn)代碼,你需要指定一個(gè)更符合你自己需求的模塊路徑。有關(guān)更多信息,請務(wù)必查看管理依賴項(xiàng)。

接下來,你將添加一些簡單的代碼來處理 maps。

添加非泛型函數(shù)

在此步驟中,你將添加兩個(gè)函數(shù),每個(gè)函數(shù)將 map 的值相加并返回總數(shù)。

你要聲明兩個(gè)函數(shù)而不是一個(gè),因?yàn)槟阏谑褂脙煞N不同類型的映射:一種用于存儲(chǔ) int64 值,另一種用于存儲(chǔ) float64 值。

編寫代碼

  1. 使用你的文本編輯器,在 generics 目錄中創(chuàng)建一個(gè)名為 main.go 的文件。你將在此文件中編寫你的 Go 代碼。

  2. 進(jìn)入 main.go,在文件頂部,粘貼以下包聲明。

package main

獨(dú)立程序(與庫相反)始終位于 packagemain。

  1. 在包聲明下方,粘貼以下兩個(gè)函數(shù)聲明。
// 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
}

在此代碼中,你:

  • 聲明兩個(gè)函數(shù)以將地圖的值相加并返回總和。
  • SumFloats stringfloat64 值的 map。
  • SumInts stringint64 值的 map。
  1. main.go 頂部的包聲明下方,粘貼以下 main 函數(shù)以初始化兩個(gè) map,并在調(diào)用你在上一步中聲明的函數(shù)時(shí)將它們用作參數(shù)。
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))
}

在此代碼中,你:

  • 初始化一個(gè) float64 map 和一個(gè) int64 map,每個(gè)都有兩個(gè)條目。
  • 調(diào)用你之前聲明的兩個(gè)函數(shù)來查找每個(gè) map 值的總和。
  • 打印結(jié)果。
  1. main.go 頂部附近,就在包聲明的下方,導(dǎo)入你需要支持你剛剛編寫的代碼的包。

第一行代碼應(yīng)如下所示:

package main

import "fmt"
  1. 保存 main.go

運(yùn)行代碼

從包含 main.go 的目錄中的命令行,運(yùn)行代碼。

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

使用泛型,你可以在此處編寫一個(gè)函數(shù)而不是兩個(gè)。接下來,你將為包含整數(shù)或浮點(diǎn)值的映射添加一個(gè)通用函數(shù)。

添加通用函數(shù)來處理多種類型

在本節(jié)中,你將添加一個(gè)通用函數(shù),該函數(shù)可以接收包含整數(shù)或浮點(diǎn)值的映射,從而有效地將你剛剛編寫的兩個(gè)函數(shù)替換為一個(gè)函數(shù)。

要支持任一類型的值,該單個(gè)函數(shù)將需要一種方法來聲明它支持的類型。另一方面,調(diào)用代碼需要一種方法來指定它是使用整數(shù)映射還是浮點(diǎn)映射

為了支持這一點(diǎn),你將編寫一個(gè)函數(shù),該函數(shù)在其普通函數(shù)參數(shù)之外還聲明類型參數(shù)。這些類型參數(shù)使函數(shù)具有通用性,使其能夠處理不同類型的參數(shù)。你將使用類型參數(shù)和普通函數(shù)參數(shù)調(diào)用該函數(shù)。

每個(gè)類型參數(shù)都有一個(gè)類型約束,它充當(dāng)類型參數(shù)的一種元類型。每個(gè)類型約束指定調(diào)用代碼可用于相應(yīng)類型參數(shù)的允許類型參數(shù)。

雖然類型參數(shù)的約束通常表示一組類型,但在編譯時(shí),類型參數(shù)代表單一類型——調(diào)用代碼作為類型參數(shù)提供的類型。如果類型參數(shù)的約束不允許類型參數(shù)的類型,則代碼將無法編譯。

請記住,類型參數(shù)必須支持泛型代碼對其執(zhí)行的所有操作。例如,如果你的函數(shù)代碼嘗試對其 string 約束包括數(shù)字類型的類型參數(shù)執(zhí)行操作(例如索引),則代碼將無法編譯。

在你即將編寫的代碼中,你將使用允許整數(shù)或浮點(diǎn)類型的約束。

編寫代碼

  1. 在你之前添加的兩個(gè)函數(shù)下方,粘貼以下通用函數(shù)。
// 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
}

在此代碼中,你:

  • 聲明一個(gè) SumIntsOrFloats 具有兩個(gè)類型參數(shù)(在方括號內(nèi))KV 的函數(shù),以及一個(gè)使用類型參數(shù)的參數(shù),m 類型為 map[K]V。該函數(shù)返回一個(gè)類型的值 V。
  • K 類型參數(shù)指定類型約束 comparable。專門針對此類情況,comparableGo 中預(yù)先聲明了約束。它允許任何類型的值可以用作比較運(yùn)算符 == 和的操作數(shù) !=Go 要求 map keys 具有可比性。所以聲明 K as comparable 是必要的,這樣你就可以 Kmap 變量中用作鍵。它還確保調(diào)用代碼對 map keys 使用允許的類型。
  • V 類型參數(shù)指定一個(gè)約束,它是兩種類型的聯(lián)合:int64float64使用 | 指定兩種類型的聯(lián)合,這意味著此約束允許任何一種類型。編譯器將允許任一類型作為調(diào)用代碼中的參數(shù)。
  • 指定 m 參數(shù)是 type map[K]V,其中 KV 是已經(jīng)為類型參數(shù)指定的類型。請注意,我們知道 map[K]V 是有效的 map 類型,因?yàn)?K 它是可比較的類型。如果我們沒有聲明 K 可比較,編譯器將拒絕對 map[K]V 的引用。

main.go 中,在你已有的代碼下方,粘貼以下代碼。

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

在此代碼中,你:

  • 調(diào)用你剛剛聲明的通用函數(shù),傳遞你創(chuàng)建的每個(gè)映射。
  • 指定類型參數(shù) - 方括號中的類型名稱 - 以明確應(yīng)該替換你正在調(diào)用的函數(shù)中的類型參數(shù)的類型。
  • 正如你將在下一節(jié)中看到的,你通常可以在函數(shù)調(diào)用中省略類型參數(shù)。Go 通常可以從你的代碼中推斷出它們。
  • 打印函數(shù)返回的總和。

運(yùn)行代碼

從包含 main.go 的目錄中的命令行,運(yùn)行代碼。

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

為了運(yùn)行你的代碼,在每次調(diào)用中,編譯器將類型參數(shù)替換為該調(diào)用中指定的具體類型。

在調(diào)用你編寫的泛型函數(shù)時(shí),你指定了類型參數(shù),告訴編譯器使用什么類型代替函數(shù)的類型參數(shù)。正如你將在下一節(jié)中看到的,在許多情況下你可以省略這些類型參數(shù),因?yàn)榫幾g器可以推斷它們。

調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù)

在本節(jié)中,你將添加通用函數(shù)調(diào)用的修改版本,進(jìn)行小的更改以簡化調(diào)用代碼。你將刪除在這種情況下不需要的類型參數(shù)。

當(dāng) Go 編譯器可以推斷你要使用的類型時(shí),你可以在調(diào)用代碼中省略類型參數(shù)。編譯器從函數(shù)參數(shù)的類型推斷類型參數(shù)。

請注意,這并不總是可能的。例如,如果你需要調(diào)用沒有參數(shù)的泛型函數(shù),則需要在函數(shù)調(diào)用中包含類型參數(shù)。

編寫代碼

main.go 中,在你已有的代碼下方,粘貼以下代碼。

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

在此代碼中,你:

  • 調(diào)用泛型函數(shù),省略類型參數(shù)。

運(yùn)行代碼

從包含 main.go 的目錄中的命令行,運(yùn)行代碼。

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

接下來,你將通過將整數(shù)和浮點(diǎn)數(shù)的并集捕獲到你可以重用的類型約束(例如從其他代碼中)來進(jìn)一步簡化函數(shù)

聲明類型約束

在最后一部分中,你將把之前定義的約束移到它自己的接口中,以便你可以在多個(gè)地方重用它。以這種方式聲明約束有助于簡化代碼,例如當(dāng)約束更復(fù)雜時(shí)。

你將類型約束聲明為接口。約束允許任何類型實(shí)現(xiàn)接口。例如,如果你聲明了具有三個(gè)方法的類型約束接口,然后在泛型函數(shù)中將其與類型參數(shù)一起使用,則用于調(diào)用該函數(shù)的類型參數(shù)必須具有所有這些方法。

正如你將在本節(jié)中看到的,約束接口也可以引用特定類型

編寫代碼

  1. 就在上面 main,緊跟在 import 語句之后,粘貼以下代碼來聲明類型約束。
type Number interface {
    int64 | float64
}

在此代碼中,你:

  • 聲明 Number 要用作類型約束的接口類型。

  • 在接口內(nèi)部聲明一個(gè)并集 int64float64

本質(zhì)上,你正在將聯(lián)合從函數(shù)聲明移動(dòng)到新的類型約束中。這樣,當(dāng)你想將類型參數(shù)約束為 int64 或者 float64 時(shí),你可以使用此 Number 類型約束而不是寫出 int64 | float64。

  1. 在你已有的函數(shù)下方,粘貼以下通用 SumNumbers 函數(shù)。
// 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
}

在此代碼中,你:

  • 聲明一個(gè)與你之前聲明的泛型函數(shù)具有相同邏輯的泛型函數(shù),但使用新的接口類型而不是聯(lián)合作為類型約束。和以前一樣,你使用類型參數(shù)作為參數(shù)和返回類型。
  1. main.go 中,在你已有的代碼下方,粘貼以下代碼。
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

在此代碼中,你:

  • 調(diào)用 SumNumbers 每個(gè) map,打印每個(gè) map 的總和。

與上一節(jié)一樣,在調(diào)用泛型函數(shù)時(shí)省略了類型參數(shù)(方括號中的類型名稱)。Go 編譯器可以從其他參數(shù)推斷類型參數(shù)。

運(yùn)行代碼

從包含 main.go 的目錄中的命令行,運(yùn)行代碼。

$ 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

結(jié)論

做得很好!你剛剛向自己介紹了 Go 中的泛型。

建議的下一個(gè)主題:

完整的代碼

你可以在 Go playground 上運(yùn)行這個(gè)程序。在 playground 上,只需單擊“運(yùn)行”按鈕。

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
}

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容