Go 泛型预览
即将发布的 Go 1.18 将会带来一个巨大的更新:泛型。
这篇文章是对官方指引的翻译。
准备
在正式尝试泛型之前,需要做如下的工作
- 安装 go1.18beta1 或更新的版本。安装方式请看 beta版本的安装与使用
- 一个可以编辑代码的编辑器,任何你喜欢的编辑器都可以
- 命令行终端,在 Linux 和 mac 上可以使用自带的 terminal,在 Windows 上可以使用 powershell 或者 cmd
安装并使用测试版 Go 工具链
由于 Go1.18 还未正式发布,因此我们需要安装测试版工具链来尝鲜,按照如下的步骤来安装:
运行下面的命令来安装测试版
1
$ go install golang.org/dl/go1.18beta2@latest
运行下面的命令来下载更新
1
$ go1.18beta2 download
使用测试版本程序取代原有的 go 命令(如果安装了正式版的 go)
可以直接使用测试版的名字,或者将
go
设置为测试版程序的别名直接使用测试版,你可以运行下面的命令来调用测试版程序来取代
go
命令:1
$ go1.18beta2 version
将
go
设置为别名,可以让测试版的使用更加简单1
2$ alias go=go1.18beta2
$ go version
之后的命令将会默认你已经设置了别名
创建一个代码文件夹
首先需要一个文件夹来存放测试代码
打开命令行,进入家目录
在 Linux 或者 Mac 上
1
$ cd
在 Windows 上
1
C:\> cd %HOMEPATH%
教程的其余部分将显示$作为提示。您使用的命令也可以在Windows上使用。
在命令行行中创建一个目录
1
2$ mkdir generics
$ cd generics创建一个新的 Go module 来存放代码
运行
go mod init
命令,并给定模块的路径1
2$ go mod init example/generics
go: creating new go.mod: module example/generics注意: 对于生产代码, 你需要指定一个与你的需求更加契合的模块路径。更多信息可以参考 Managing dependencies.
接下来,你将会写一些简单的代码来接触泛型
新增一个非泛型函数
在这一步,你将会增加两个函数,每个函数都会将 map 中的值相加,然后返回最终的和。
之所以会定义两个函数,是因为有两种不同类型的 map 需要被处理:一种的值类型为 int64,另一种的值类型则是 float64.
编写代码
用你的编辑器在目录下创建一个
main.go
文件,你将会在这个文件中创建代码在 main.go 文件的最顶部,粘贴下面的包声明语句
1
package main
一个独立的可执行程序(而不是库)入口总是在 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
}在上述的代码中,你做了如下的事:
- 定义了两个函数,每个函数都会将 map 中的值相加,然后返回最终的和。
- SumInts 处理 string-int64 类型的 map
- SumFloats 处理 string-float64 类型的 map
- 定义了两个函数,每个函数都会将 map 中的值相加,然后返回最终的和。
在 main.go 中,包声明语句的下方粘贴 main 方法的代码。在 main 方法中,会初始化两个 map,并且将它们作为参数传入在上一步中定义的方法内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func 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 中,就在包定义语句的下方,导入刚刚书写的代码中所需要的包。
第一行代码应该如下所示:
1
2
3pacakge main
import "fmt"保存 main.go
运行代码
在包含 main.go 的目录中的命令行中,运行代码:
1 |
|
通过使用泛型,你可以使用一个函数来完成需求。在接下来的步骤中,将会添加一个函数来处理值类型为 int64 或 float64 的 map。
新增一个泛型函数来处理多种类型
在这一节,你将会添加一个泛型函数,它接受值类型为 float64 或 int64 的 map 作为参数,使用单个函数就可以高效的替换原有的两个函数。
为了支持任一类型的值,这个函数需要一种方法来声明它支持的类型。另一方面,函数的调用者需要一种方法来指定传入的 map 是float64 还是 int64 的。
为了支持这一点,你将编写一个函数,除了普通的函数参数外,还会声明类型参数。这些类型参数使函数具有通用性(泛型),使其能够处理不同类型的参数。你将使用类型参数和普通函数参数调用函数。
每个参数类型都有着一个类型约束(type constraint ),作为参数类型的元类型(meta-type)。每个类型约束都指定了,在调用函数时所允许传入的参数类型。
虽然类型约束通常代表了一组类型,但在编译时,类型参数代表了单个类型——函数调用者提供的参数类型的类型。如果类型参数的类型不符合类型约束,那么代码将无法编译。
请记住,类型参数必须支持泛型代码在该类型上执行的所有操作。例如,如果函数的代码试图对约束包含 int 类型参数执行 string 操作(如 strings.Index ),那么代码将无法通过编译。
在即将编写的代码中,你将会使用允许 int 以及 float 的类型约束。
编写代码
在上一节新增的代码下方,粘贴泛型函数的代码
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,以及一个类型为 map[K]V 的参数 m。函数的返回值类型为 V
- 指定 K 的类型约束为 comparable。 comparable 是 go 预设的一个类型约束。它允许任何满足约束的类型的值都可以用作比较运算符 == 和 !=。这是因为在 Go 中 map 的 key 的类型必须是可比较的。因此将 K 设为 comparable 就是非常必要的了,你可以使用 K 来作为 map 的key。同时保证了调用者只使用被允许的类型的 key。
- 指定 V 的类型约束为 int64 和 float64 两种类型的联合类型。使用操作符|指定两种类型的联合类型,表示该约束允许其中的任意一种类型。编译器允许这两种类型作为参数的类型传入函数。
- 指定了 m 的类型为 map[K]V,K,V 的类型已经被类型参数所指定了。需要注意的是, map[K]V 是一个合法的 map 类型,因为K 已经被声明为 comparable 了。如果没有将 K 声明为 comparable,编译器将拒绝 map[K]V 的使用
在 main.go 中,粘贴下面的代码到原有代码的下方
1
2
3fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))在这段代码中:
调用了刚才定义的泛型函数,传入了创建的 map
指定了类型参数 – 包含在方括号中的类型的名称 - 表明在调用时需要被替换的类型参数的类型
正如下一节中看到的,通常可以在函数调用中省略类型参数。Go编译器通常可以从你的代码中推断它们。
打印函数的返回值
运行代码
在包含 main.go 的目录中的命令行中,运行代码:
1 |
|
要运行代码,在每次调用中,编译器都会将类型参数替换为该调用中指定的具体类型。
在调用泛型函数时,通过指定类型参数,来告诉编译器使用哪些类型来代替函数的类型参数。
在调用泛型函数时移除类型参数
在本节中,你将添加泛型函数调用的修改版本,进行小幅更改以简化调用代码。将会删除类型参数,在这种情况下,这些参数不需要。
当Go编译器可以推断要使用的类型时,可以在调用代码时省略类型参数。编译器从函数参数的类型中推断类型参数。
请注意,这并不总是可行的。例如,如果在需要调用没有参数的泛型函数时,需要在函数调用中包含类型参数。
编写代码
在 main.go 中,粘贴下面的代码到原有代码的下方
1
2
3fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))在这段代码中:
- 调用了泛型函数,并省略了类型参数
运行代码
在包含 main.go 的目录中的命令行中,运行代码:
1 |
|
接下来,你会定义一个可以重用的类型约束,该约束的类型为整数和浮点的联和类型,通过在代码中使用该约束来进一步简化代码。
定义类型约束
在最后这一节,你将会把之前定义的约束放进它自己的 interface 中,这样的话就可以在不同的地方重用它了。以这种方式声明约束有助于简化代码,例如当约束更复杂时。
你可以将类型约束定义为一个 interface。约束允许任何类型去实现接口。例如,声明了一个具有三种方法的类型约束接口,然后将其与泛型函数中的类型参数一起使用,则用于调用函数的类型参数必须具有所有的这些方法。
约束接口也可以引用特定类型,您将在本节中看到。
编写代码
就在 main 函数的上方,在导入语句之后,粘贴以下代码以声明类型约束。
1
2
3type Number interface {
int64 | float64
}在这段代码中:
声明要用作类型约束的Number接口类型。
在接口内部声明了 int64 | float64 的联合类型。
本质上,是将联合类型从函数声明移动到新的类型约束中。这样,当你想将类型参数限制为int64或float64时,就可以使用此Number 类型约束,而不是写出 int64 |float64。
在原有函数的下方,粘贴下面的代码
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
3fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))在这段代码中:
对每个 map 调用 SumNumbers,并打印每个结果。
与上一节一样,在对泛型函数的调用中省略了类型参数(方括号内的类型名称)。Go编译器可以从其他参数推断类型参数。
运行代码
在包含 main.go 的目录中的命令行中,运行代码:
1 |
|
完整代码
1 |
|