前言
本教程介绍 Go 中泛型的基础知识。使用泛型,你可以声明和使用编写为与调用代码提供的任何一组类型一起使用的函数或类型。
在本教程中,你将声明两个简单的非泛型函数,然后在单个泛型函数中实现相同的逻辑。
你将逐步完成以下部分:
- 为你的代码创建一个文件夹。
- 添加非泛型函数。
- 添加一个通用函数来处理多种类型。
- 调用泛型函数时删除类型参数。
- 声明类型约束。
注意:有关其他教程,请参阅教程。
注意:如果你愿意,可以使用 “Go dev 分支”模式下的 Go Playground 来编辑和运行你的程序。
先决条件
Go 1.18
或更高版本的安装。有关安装说明,请参阅安装 Go。- 用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
- 一个命令终端。Go 在 Linux 和 Mac 上的任何终端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。
为你的代码创建一个文件夹
首先,为你要编写的代码创建一个文件夹。
- 打开命令提示符并切换到你的主目录。
在 Linux 或 Mac 上:
1
$ cd
在 Windows 上:
1
C:\> cd %HOMEPATH%
本教程的其余部分将显示 $
作为提示。你使用的命令也可以在 Windows
上运行。
- 在命令提示符下,为你的代码创建一个名为
generics
的目录。
1
2
$ mkdir generics
$ cd generics
- 创建一个模块来保存你的代码。
运行 go mod init
命令,为其提供新代码的模块路径。
1
2
$ go mod init example/generics
go: creating new go.mod: module example/generics
注意:对于生产代码,你需要指定一个更符合你自己需求的模块路径。有关更多信息,请务必查看管理依赖项。
接下来,你将添加一些简单的代码来处理 maps
。
添加非泛型函数
在此步骤中,你将添加两个函数,每个函数将 map
的值相加并返回总数。
你要声明两个函数而不是一个,因为你正在使用两种不同类型的映射:一种用于存储 int64
值,另一种用于存储 float64
值。
编写代码
-
使用你的文本编辑器,在
generics
目录中创建一个名为main.go
的文件。你将在此文件中编写你的Go
代码。 -
进入
main.go
,在文件顶部,粘贴以下包声明。
1
package main
独立程序(与库相反)始终位于 package
中 main
。
- 在包声明下方,粘贴以下两个函数声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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
string
到float64
值的map
。SumInts
string
到int64
值的map
。
- 在
main.go
顶部的包声明下方,粘贴以下main
函数以初始化两个map
,并在调用你在上一步中声明的函数时将它们用作参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 值的总和。
- 打印结果。
- 在
main.go
顶部附近,就在包声明的下方,导入你需要支持你刚刚编写的代码的包。
第一行代码应如下所示:
1
2
3
package main
import "fmt"
- 保存
main.go
。
运行代码
从包含 main.go
的目录中的命令行,运行代码。
1
2
$ go run .
Non-Generic Sums: 46 and 62.97
使用泛型,你可以在此处编写一个函数而不是两个。接下来,你将为包含整数或浮点值的映射添加一个通用函数。
添加通用函数来处理多种类型
在本节中,你将添加一个通用函数,该函数可以接收包含整数或浮点值的映射,从而有效地将你刚刚编写的两个函数替换为一个函数。
要支持任一类型的值,该单个函数将需要一种方法来声明它支持的类型。另一方面,调用代码需要一种方法来指定它是使用整数映射还是浮点映射。
为了支持这一点,你将编写一个函数,该函数在其普通函数参数之外还声明类型参数。这些类型参数使函数具有通用性,使其能够处理不同类型的参数。你将使用类型参数和普通函数参数调用该函数。
每个类型参数都有一个类型约束,它充当类型参数的一种元类型。每个类型约束指定调用代码可用于相应类型参数的允许类型参数。
虽然类型参数的约束通常表示一组类型,但在编译时,类型参数代表单一类型——调用代码作为类型参数提供的类型。如果类型参数的约束不允许类型参数的类型,则代码将无法编译。
请记住,类型参数必须支持泛型代码对其执行的所有操作。例如,如果你的函数代码尝试对其 string 约束包括数字类型的类型参数执行操作(例如索引),则代码将无法编译。
在你即将编写的代码中,你将使用允许整数或浮点类型的约束。
编写代码
- 在你之前添加的两个函数下方,粘贴以下通用函数。
1
2
3
4
5
6
7
8
9
// 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
。 - 为
K
类型参数指定类型约束comparable
。专门针对此类情况,comparable
在Go
中预先声明了约束。它允许任何类型的值可以用作比较运算符==
和的操作数!=
。Go
要求map keys
具有可比性。所以声明K as comparable
是必要的,这样你就可以K
在map
变量中用作键。它还确保调用代码对map keys
使用允许的类型。 - 为
V
类型参数指定一个约束,它是两种类型的联合:int64
和float64
。使用|
指定两种类型的联合,这意味着此约束允许任何一种类型。编译器将允许任一类型作为调用代码中的参数。 - 指定
m
参数是type map[K]V
,其中K
和V
是已经为类型参数指定的类型。请注意,我们知道map[K]V
是有效的map
类型,因为K
它是可比较的类型。如果我们没有声明K
可比较,编译器将拒绝对map[K]V
的引用。
在 main.go
中,在你已有的代码下方,粘贴以下代码。
1
2
3
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
在此代码中,你:
- 调用你刚刚声明的通用函数,传递你创建的每个映射。
- 指定类型参数 - 方括号中的类型名称 - 以明确应该替换你正在调用的函数中的类型参数的类型。
- 正如你将在下一节中看到的,你通常可以在函数调用中省略类型参数。
Go
通常可以从你的代码中推断出它们。 - 打印函数返回的总和。
运行代码
从包含 main.go
的目录中的命令行,运行代码。
1
2
3
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
为了运行你的代码,在每次调用中,编译器将类型参数替换为该调用中指定的具体类型。
在调用你编写的泛型函数时,你指定了类型参数,告诉编译器使用什么类型代替函数的类型参数。正如你将在下一节中看到的,在许多情况下你可以省略这些类型参数,因为编译器可以推断它们。
调用泛型函数时删除类型参数
在本节中,你将添加通用函数调用的修改版本,进行小的更改以简化调用代码。你将删除在这种情况下不需要的类型参数。
当 Go
编译器可以推断你要使用的类型时,你可以在调用代码中省略类型参数。编译器从函数参数的类型推断类型参数。
请注意,这并不总是可能的。例如,如果你需要调用没有参数的泛型函数,则需要在函数调用中包含类型参数。
编写代码
在 main.go
中,在你已有的代码下方,粘贴以下代码。
1
2
3
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
在此代码中,你:
- 调用泛型函数,省略类型参数。
运行代码
从包含 main.go
的目录中的命令行,运行代码。
1
2
3
4
$ 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
语句之后,粘贴以下代码来声明类型约束。
1
2
3
type Number interface {
int64 | float64
}
在此代码中,你:
-
声明
Number
要用作类型约束的接口类型。 -
在接口内部声明一个并集
int64
和float64
本质上,你正在将联合从函数声明移动到新的类型约束中。这样,当你想将类型参数约束为 int64
或者 float64
时,你可以使用此 Number
类型约束而不是写出 int64 | float64
。
- 在你已有的函数下方,粘贴以下通用
SumNumbers
函数。
1
2
3
4
5
6
7
8
9
// 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
中,在你已有的代码下方,粘贴以下代码。
1
2
3
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
在此代码中,你:
- 调用
SumNumbers
每个map
,打印每个map
的总和。
与上一节一样,在调用泛型函数时省略了类型参数(方括号中的类型名称)。Go
编译器可以从其他参数推断类型参数。
运行代码
从包含 main.go
的目录中的命令行,运行代码。
1
2
3
4
5
$ 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 Tour是对
Go
基础知识的逐步介绍。 - 你将在 Effective Go 和 How to write Go code 中找到有用的
Go
最佳实践。
完整的代码
你可以在 Go playground 上运行这个程序。在 playground
上,只需单击“运行”按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
}