教程:使用泛型入门

本教程介绍了 Go 中泛型的基础知识。使用泛型,您可以声明和使用编写为可与调用代码提供的任何一组类型一起使用的函数或类型。

在本教程中,您将声明两个简单的非泛型函数,然后在一个泛型函数中捕获相同的逻辑。

您将学习以下部分

  1. 为您的代码创建一个文件夹。
  2. 添加非泛型函数。
  3. 添加一个泛型函数来处理多种类型。
  4. 在调用泛型函数时移除类型参数。
  5. 声明类型约束。

注意:有关其他教程,请参见 教程

注意:如果您愿意,可以使用 “Go 开发分支”模式下的 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 采用一个从 stringfloat64 值的映射。
      • SumInts 采用一个从 stringint64 值的映射。
  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,以及一个使用类型参数 m 的参数,其类型为 map[K]V。此函数返回 V 类型的数值。
    • K 类型参数指定类型约束 comparablecomparable 约束是专门为这种情况预先声明的,它允许任何类型的数值用作比较运算符 ==!= 的操作数。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 的并集。

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