教程:泛型入门

本教程介绍 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
    

    注意: 对于生产代码,您需要指定一个更符合您自己需求的模块路径. 有关更多信息,请务必查看 管理依赖项.

接下来,您将添加一些简单的代码来处理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的值相加并返回总和.
      • SumFloatsstring映射为 float64 值.
      • SumIntsstring 映射为 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

使用泛型,您可以在此处编写一个函数,而不是两个。接下来,您将为包含整数或浮点值的映射添加单个泛型函数.

添加一个泛型函数来处理多种类型

在本节中,您将添加一个可以接收包含整数或浮点值的映射的泛型函数,从而有效地将刚刚编写的两个函数替换为单个函数.

若要支持任一类型的值,该单个函数将需要一种方法来声明它支持的类型。另一方面,调用代码需要一种方法来指定它是使用整数映射还是浮点映射进行调用.

为了支持这一点,您将编写一个函数,该函数除了声明其普通函数参数外,还声明 类型参数 。这些类型参数使函数成为泛型函数,使其能够与不同类型的参数一起使用。您将使用 类型参数 和普通函数参数调用该函数。

每个类型参数都有一个 类型约束 ,该约束充当类型参数的一种元类型。每个类型约束指定允许的类型参数,调用代码可以将其用于相应的类型参数.

虽然类型参数的约束通常表示一组类型,但在编译时,类型参数代表单个类型 - 由调用代码作为类型参数提供的类型。如果类型参数的约束不允许类型参数的类型,则代码将不会编译.

请记住,类型参数必须支持泛型代码对其执行的所有操作。例如,如果函数的代码尝试对其约束包含数值类型的类型参数执行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,以及一个使用 map[K]V 类型的类型参数 m 。该函数返回类型为 V 的值.
    • K 类型参数指定 comparable的类型约束。专门针对此类情况, comparable的约束在 Go 中预先声明。它允许其值可用作比较运算符==!= 的操作数的任何类型。Go 要求映射键具有可比性。因此,将 K 声明为comparable是必要的,以便您可以使用 K 作为映射变量中的键。它还确保调用代码对映射键使用允许的类型.
    • V 类型参数指定一个约束,该约束是两种类型的并集: int64float64。使用|指定两种类型的并集,这意味着此约束允许任一类型。编译器将允许任一类型作为调用代码中的参数.
    • 指定 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 的正上方,紧跟在 import 语句之后,粘贴以下代码以声明类型约束.

    type Number interface {
        int64 | float64
    }
    

    在此代码中,您:

    • 声明要用作类型约束的 Number 接口类型.

    • 在接口内声明 int64float64 的联合.

      实质上,您将union从函数声明移动到新的类型约束中。这样,当您想要将类型参数约束为 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
    }
    

    在此代码中,您:

    • 声明一个与您之前声明的泛型函数具有相同逻辑的泛型函数,但使用新的接口类型而不是union作为类型约束。和以前一样,您使用类型参数作为参数和返回类型。
  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
}