教程:泛型入门
本教程将介绍 Go 中泛型的基础知识。使用泛型,您可以声明和使用为与调用代码提供的任何一组类型协同工作而编写的函数或类型。
在本教程中,您将声明两个简单的非泛型函数,然后将相同的逻辑捕获到一个泛型函数中。
您将按以下章节进行学习:
- 为您的代码创建一个文件夹。
- 添加非泛型函数。
- 添加一个泛型函数来处理多种类型。
- 调用泛型函数时移除类型参数。
- 声明类型约束。
注意: 有关其他教程,请参阅教程。
注意: 如果您愿意,可以使用 “Go 开发分支”模式的 Go playground 来编辑和运行您的程序。
先决条件
- 安装 Go 1.18 或更高版本。 有关安装说明,请参阅 安装 Go。
- 一个代码编辑工具。 任何文本编辑器都可以。
- 命令终端。 Go 在 Linux 和 Mac 上的任何终端以及 Windows 上的 PowerShell 或 cmd 中都能很好地工作。
为您的代码创建一个文件夹
首先,为您的代码创建一个文件夹。
-
打开命令提示符并切换到您的主目录。
在 Linux 或 Mac 上
$ cd
在 Windows 上
C:\> cd %HOMEPATH%
本教程的其余部分将显示 $ 作为提示符。您使用的命令在 Windows 上也适用。
-
在命令提示符下,创建一个名为 generics 的代码目录。
$ mkdir generics $ cd generics
-
创建一个模块来存放您的代码。
运行
go mod init
命令,并提供新代码的模块路径。$ go mod init example/generics go: creating new go.mod: module example/generics
注意: 对于生产代码,您会指定一个更符合您自身需求的模块路径。有关更多信息,请参阅 管理依赖项。
接下来,您将添加一些简单的代码来处理 map。
添加非泛型函数
在此步骤中,您将添加两个函数,每个函数都将 map 的值相加并返回总和。
您声明了两个函数而不是一个,因为您处理的是两种不同类型的 map:一种存储 int64
值,另一种存储 float64
值。
编写代码
-
使用您的文本编辑器,在 generics 目录中创建一个名为 main.go 的文件。您将在该文件中编写 Go 代码。
-
在 main.go 文件的顶部,粘贴以下包声明。
package main
独立程序(与库相对)始终位于 `main` 包中。
-
在 package 声明下方,粘贴以下两个函数声明。
// 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 的值并返回总和。
SumFloats
接收一个从string
到float64
值的 map。SumInts
接收一个从string
到int64
值的 map。
- 声明两个函数来相加 map 的值并返回总和。
-
在 main.go 的顶部,package 声明下方,粘贴以下
main
函数,以初始化两个 map 并将它们作为参数传递给您在上一步中声明的函数。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
值 map 和一个int64
值 map,每个 map 包含两个条目。 - 调用您之前声明的两个函数来查找每个 map 值的总和。
- 打印结果。
- 初始化一个
-
在 main.go 的顶部附近,package 声明正下方,导入您需要支持您刚刚编写的代码的包。
代码的第一行应如下所示:
package main import "fmt"
-
保存 main.go。
运行代码
在包含 main.go 的目录的命令行中,运行代码。
$ go run .
Non-Generic Sums: 46 and 62.97
使用泛型,您可以编写一个函数而不是两个。接下来,您将添加一个单一的泛型函数来处理包含整数或浮点数值的 map。
添加一个泛型函数来处理多种类型
在本节中,您将添加一个单一的泛型函数,该函数可以接收包含整数或浮点数值的 map,从而有效地用一个函数替换您刚写的两个函数。
为了支持这两种类型的值,该单一函数需要一种方法来声明它支持的类型。另一方面,调用代码需要一种方法来指定它是使用整数 map 还是浮点 map 进行调用。
为了支持这一点,您将编写一个函数,该函数除了普通的函数参数外,还声明类型参数。这些类型参数使函数成为泛型函数,使其能够使用不同类型的参数。您将使用类型参数和普通函数参数来调用该函数。
每个类型参数都有一个类型约束,它充当该类型参数的一种元类型。每个类型约束都指定调用代码可以为相应类型参数使用的允许的类型参数。
虽然类型参数的约束通常代表一组类型,但在编译时,类型参数代表单个类型——调用代码作为类型参数提供的类型。如果类型参数的类型不被类型参数的约束允许,代码将无法编译。
请记住,类型参数必须支持泛型代码正在对其执行的所有操作。例如,如果您的函数代码尝试对包含数字类型的类型参数执行 string
操作(例如索引),则代码将无法编译。
在您即将编写的代码中,您将使用一个允许整数或浮点类型的约束。
编写代码
-
在您之前添加的两个函数下方,粘贴以下泛型函数。
// 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
,以及一个使用类型参数的参数,m
,类型为map[K]V
。该函数返回一个V
类型的 V alue。 - 为
K
类型参数指定comparable
类型约束。comparable
约束专为此类情况设计,它在 Go 中是预先声明的。它允许任何其值可以用作比较运算符==
和!=
的操作数的类型。Go 要求 map 的键必须是可比较的。因此,将K
声明为comparable
是必要的,以便您可以使用K
作为 map 变量的键。它还确保调用代码使用允许的类型作为 map 键。 - 为
V
类型参数指定一个由两种类型组成的联合约束:int64
和float64
。使用|
指定两种类型的联合,这意味着该约束允许这两种类型中的任何一种。编译器将允许这两种类型中的任何一种作为调用代码中的参数。 - 指定
m
参数的类型为map[K]V
,其中K
和V
是已为类型参数指定的类型。请注意,我们知道map[K]V
是一个有效的 map 类型,因为K
是一个可比较的类型。如果我们没有声明K
是可比较的,编译器将拒绝引用map[K]V
。
- 声明一个
-
在 main.go 中,在您已有的代码下方,粘贴以下代码。
fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats))
在此代码中,您
-
调用您刚刚声明的泛型函数,传递您创建的每个 map。
-
指定类型参数——方括号中的类型名称——以清楚地说明应替换被调用函数中类型参数的类型。
正如您将在下一节中看到的,您通常可以省略调用泛型函数时的类型参数。Go 通常可以从您的代码中推断出它们。
-
打印函数返回的总和。
-
运行代码
在包含 main.go 的目录的命令行中,运行代码。
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
为了运行您的代码,在每次调用时,编译器都将类型参数替换为该调用中指定的具体类型。
在调用您编写的泛型函数时,您指定了类型参数,告诉编译器在函数的类型参数中应使用哪些类型。正如您将在下一节中看到的,在许多情况下,您可以省略这些类型参数,因为编译器可以推断出它们。
调用泛型函数时移除类型参数
在本节中,您将添加一个修改后的泛型函数调用版本,进行一个小更改以简化调用代码。您将删除在这种情况下不需要的类型参数。
当 Go 编译器可以推断出您要使用的类型时,您可以在调用代码中省略类型参数。编译器会从函数参数的类型中推断出类型参数。
请注意,这并非总是可能的。例如,如果您需要调用一个没有任何参数的泛型函数,您将需要在函数调用中包含类型参数。
编写代码
-
在 main.go 中,在您已有的代码下方,粘贴以下代码。
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats))
在此代码中,您
- 调用泛型函数,省略类型参数。
运行代码
在包含 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
接下来,您将通过将整数和浮点数的联合捕获到一个可重用的类型约束中(例如,从其他代码中)来进一步简化该函数。
声明类型约束
在最后一个部分,您将把之前定义的约束移到它自己的接口中,以便您可以在多个地方重用它。以这种方式声明约束有助于简化代码,例如当约束更复杂时。
您将类型约束声明为一个接口。该约束允许实现该接口的任何类型。例如,如果您声明了一个带有三个方法的类型约束接口,然后将其与泛型函数中的类型参数一起使用,则用于调用函数的类型参数必须具有所有这些方法。
约束接口也可以引用特定类型,正如您在本节中将看到的。
编写代码
-
在
main
函数正上方,紧跟在 import 语句之后,粘贴以下代码来声明一个类型约束。type Number interface { int64 | float64 }
在此代码中,您
-
声明
Number
接口类型以用作类型约束。 -
在接口内声明
int64
和float64
的联合。本质上,您正在将联合从函数声明移到一个新的类型约束中。这样,当您想将类型参数约束为
int64
或float64
时,您可以使用这个Number
类型约束,而不是写出int64 | float64
。
-
-
在您已有的函数下方,粘贴以下泛型
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 }
在此代码中,您
- 声明一个与您之前声明的泛型函数具有相同逻辑的泛型函数,但使用新的接口类型而不是联合作为类型约束。与之前一样,您使用类型参数作为参数和返回类型。
-
在 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 教程是 Go 基础知识的绝佳循序渐进介绍。
- 您可以在 Effective Go 和 How to write Go code 中找到有用的 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
}