2021年12月14日,Golang官方发布了1.18的第一个预览版本1.18beta1,并且罕见地在官方Blog中提到了这一预览版本(传送门),可见,作为Go1中最大的单一语言变化,泛型引起了官方的足够重视。
在该篇Blog中,Russ Cox也提到,同步发布了一篇关于泛型极为详尽的真·入门教程,这可是官方教程哦,于是我(RicheyJang)赶紧翻译了一波,让大家尝尝鲜。
Go泛型入门教程
注意:以下为Beta版本内容。
本教程将介绍Go泛型的基本用法,你可以使用泛型来声明和使用由调用者提供的任意一种或多种函数或类型。
在本教程中,你将声明两个没有使用泛型的简单函数,随后,在单个泛型函数中实现相同的逻辑。
你将学习以下内容:
- 为你的代码创建文件夹
- 添加无泛型函数
- 添加一个泛型函数来处理多种类型
- 调用泛型函数时不指定类型参数
- 声明一个类型约束
注意:你可以在Go playground中使用Go dev branch模式来编辑和运行你的泛型代码。
准备工作
- 安装Go 1.18beta1:关于安装,可以查阅安装Beta版本一节。
- 一个代码编辑工具:任意文本编辑器都可以胜任。
- 一个命令行终端:Go 适用于 Linux 和 Mac 上的任何终端,以及 Windows 中的 PowerShell 或 cmd。
安装Beta版本
本教程需要使用1.18 Beta1版本中提供的泛型功能,想要安装Beta版本,请执行以下命令:
- 运行以下命令安装Beta版本的Go:
go install golang.org/dl/go1.18beta1@latest
- 运行以下命令下载包更新:
go1.18beta1 download
- 使用Beta版本来运行go命令,而非使用Release版本。
你可以使用Beta原名称来运行命令,也可以将其重命名或使用别名。- 通过使用go1.18beta1命令代替go命令来使用Beta原名运行go:
go1.18beta1 version
- 通过将Beta名称重命名,可以简化命令:
alias go=go1.18beta1 go version
- 通过使用go1.18beta1命令代替go命令来使用Beta原名运行go:
本教程中所出现的命令将假定你将go1.18beta1
重命名为了go
。
为你的代码创建文件夹
首先,请为你的项目创建一个文件夹。
- 打开你的终端并进入Home文件夹。
# Linux 或 MAc cd # Windows cd %HOMEPATH%
- 为你的代码创建一个名为
generics
的文件夹。mkdir generics cd generics
- 初始化Go Module。
注意:对于生产环境的代码,你应该更为具体地指定module路径,参阅依赖管理。go mod init example/generics
接下来,你将编写一些简单的代码来使用各种map。
添加无泛型函数
在这一步中,你将添加两个计算map中所有值之和的函数。
由于你需要处理两种map:一种值类型为int64、另一种值类型为float64,因此,需要声明两个不同的函数。
编写代码
使用你的文本编辑器,在 generics 目录中创建一个名为 main.go 的文件。你将在此文件中编写 Go 代码。
package main
// SumInts 计算m的value之和
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats 计算m的value之和
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
在上述代码中,你声明了两个函数来计算map的value之和:
- SumInts 用于计算string到int64的map。
- SumFloats 用于计算string到float64的map。
随后,在main.go顶部的包声明下方,声明main函数来初始化两个map,并调用上述两个函数。
import "fmt"
func main() {
// 为int64类型初始化一个 map
ints := map[string]int64{
"first": 34,
"second": 12,
}
// 为float64类型初始化一个 map
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
}
运行代码
在包含main.go的目录中执行:
go run .
你将看到以下结果:
Non-Generic Sums: 46 and 62.97
若使用泛型,在这一场景下,你将仅需编写一个函数来完成同样的工作。接下来,你将添加单个函数,它能够同时处理value类型为int64及float64的map。
添加一个泛型函数来处理多种类型
在这一节中,你将添加一个可以接收value类型为int64或float64的map作为参数的泛型函数,来代替上述两个函数。
为支持特定的value类型,该函数需要一种方式来指定它支持哪些类型。另一方面,在调用该函数时,也需要一种方式来指定此时具体使用了哪种类型。
为做到这一点,你在编写该函数时,除了要声明它的一般形参之外,还需要声明它的类型形参type parameters
。类型形参使得该函数支持泛型,使其可以处理多种参数类型。调用该函数时,将指定它的类型实参type arguments
以及一般实参。
每个类型形参都有一个类型约束type constraint
,作为类型形参的一种元类型。每一个类型约束用于指定调用时相应类型形参所允许使用的类型实参。如果传入的类型实参不满足类型约束的要求,代码将无法编译通过。
记住!一个类型形参必须保证它支持泛型代码中针对它进行的任意一种运算。举个栗子,如果你尝试对约束包含数值类型的类型形参执行字符串操作(例如索引),代码将无法编译通过。
接下来的代码,你将使用一种包含了int64和float64类型的类型约束。
编写代码
在此前的两个Sum函数之后,添加以下泛型函数:
// SumIntsOrFloats 计算m的value之和. 它同时支持int64及float64来作为map的value类型。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
在上述代码中,你完成了以下工作:
- 声明了一个包含两个类型形参(位于方括号中)
K
、V
的SumIntsOrFloats
函数,同时,m
使用了这些类型形参来作为它的类型map[K]V
,该函数返回一个V
类型的值。 - 指定
K
的类型约束为comparable
。针对这类情况,Go中预先声明了comparable
约束,它包含了允许值进行==
和!=
比较操作的任意类型。Go要求map的key可比较,因此,作为map的key,声明K
的类型约束为comparable
是必要的。它同时也保证了调用者使用一个可比较类型来作为map的key。 - 指定
V
的类型约束包含两种类型:int64
和float64
,使用|
来指定这两种类型的集合union
,代表了这一约束允许这两种类型。这两种类型都被编译器允许作为调用时的参数。 - 指定
m
参数的类型为map[K]V
,K和V为此前指定的类型形参。注意,由于K为可比较的,因此map[K]V
是有效的。若我们没有声明K为可比较的,编译器将拒绝map[K]V
的引用。
在main函数的最后,添加以下代码:
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints), // ints 为此前main中声明的一个map[string]int64
SumIntsOrFloats[string, float64](floats)) // floats 为此前main中声明的一个map[string]float64
在上述代码中,你完成了以下工作:
- 调用你所声明的泛型函数,并将你所创建的两个map作为参数进行了传递。
- 指定了类型实参(方括号中的类型名),来明确替换你正在调用的函数中类型形参的类型。
正如你在下一节中可以看到的,调用泛型函数时,你通常可以省略类型实参,Go会进行相应的推导。 - 打印该函数计算出的值。
运行代码
在包含main.go的目录中执行:
go run .
你将看到以下结果:
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
在每次函数调用时,编译器都会用该调用中指定的具体类型替换类型形参。
在调用泛型函数时,你指定了类型实参来告知编译器使用何种类型来替换类型形参。在下一节中,你将发现,在大多数情况下,由于编译器可以推导类型实参,你可以省略这些类型实参。
调用泛型函数时不指定类型参数
在这一节中,你将继续添加调用泛型函数的修改版本,移除不需要的类型实参,以简化调用代码。
当Go编译器可以推导出你想要使用的类型时,你在调用泛型代码时便可以省略类型实参了。编译器通过传入的一般实参类型来推导类型实参。
注意,这不是一直可行的。举个栗子,如果你需要调用一个没有参数的泛型函数,你就需要在调用该函数时包含类型实参了。
编写代码
在main函数的最后,添加以下代码:
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
接下来,你将通过整合整数和实数的集合到一个可重用的类型约束(例如来自其它包)中来进一步简化该函数。
声明一个类型约束
在这一节中,你将把此前在函数声明中所定义的约束移动到它自己的接口interface
中,以使得你可以复用它。当约束愈加复杂时,你可以使用这种方式来精简代码。
一个类型约束type constraint
使用接口interface
来进行声明。这一约束接受实现了这一接口的任意类型。举个栗子,你声明了一个包含三个方法的类型约束,随后在一个泛型函数中把它作为一个类型形参,调用该函数时使用的类型实参就必须拥有前述三个方法。
正如你在本节中所见的,约束接口也可以引用特定的类型:
编写代码
在main函数之上,紧接import语句之后,添加以下代码来声明一个类型约束:
type Number interface {
int64 | float64
}
在上述代码中,你完成了以下工作:
- 声明了一个
Number
接口来作为一个类型约束。 - 在该接口中,声明了一个int64和float64的集合
union
。
本质上来说,你正在将该集合从函数声明中移动到一个全新的类型约束中。通过这种方式,当你想要约束一个类型形参为int64类型或float64类型时,你可以使用Number
约束,而不是直接写出int64 | float64
。
在你所声明的一系列函数之后,添加一个泛型函数SumNumbers:
// SumNumbers s计算m的value之和. 它同时支持int64及float64来作为map的value类型。
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
在上述代码中,你完成了以下工作:
- 声明了一个与SumIntsOrFloats函数具有相同逻辑的泛型函数,但是使用了新的接口类型替换了原来的集合来作为类型约束。与此前一样,将类型形参用于实参和返回类型。
在main函数的最后,添加以下代码:
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
在上述代码中,你完成了以下工作:
- 使用每一个map调用了SumNumbers函数,打印它们的value之和。
与上一节一样,在调用泛型函数时省略了类型参数(方括号中的类型名称)。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中的泛型。
如果你想继续实验,你可以尝试依据constraints
包中的constraints.Integer
和constraints.Float
重写Number
接口,来支持更多的数值类型。
下一步的建议:
- Go Tour是一个很好的Go原理逐步介绍
- 你可以在Effective Go和How to write Go code中找到更多有用的Go语言练习。
完整代码
你可以在Go 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
}
译者注
人工逐字翻译不易,如需转载,请在询问我(RicheyJang)允许后,标注上文章来源,谢谢!