一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
本文翻译自:
Tutorial: Getting started with generics - The Go Programming Language
指南:开始使用泛型
内容
该指南介绍了 go 中泛型的基础。使用泛型, 可以声明和使用函数或类型,它们编写用于使用任意类型的集合来调用代码。
在该指南中,你会声明两个非泛型函数,然后提取共同的逻辑到单个泛型函数中。
你会通过以下几部分来完成:
- 为代码创建文件夹。
- 添加非泛型函数。
- 添加泛型函数处理多种类型。
- 调用泛型函数时移除类型参数。
- 声明类型约束。
注意: 其它指南查看 教程。
注意: 如果你喜欢,可以使用 “Go dev branch” 模式的 Go playground 代替来编辑和运行程序。
前提
- 安装 Go 1.18 或更高版本。 安装说明查看 Installing Go。
- 编辑代码的工具。 平时使用的任何文本编辑器都可以。
- 一个命令终端 Go 在 Liunx 或 Mac 的任何终端上在 Windows 上的 PowerShell 或 cmd 上都能正常使用。
为代码创建文件夹
首先,为要编写的代码创建文件夹。
-
打开终端命令行,进入到 home 目录。
在 Linux 或 Mac 上:
$ cd在 Windows 上:
C:> cd %HOMEPATH%下面的内容会显示 $ 作为提示符。你使用的命令在 Windows 上也能正常工作。
-
在命令行,为调用泛型的代码创建目录。
$ mkdir generics $ cd generics -
创建模块管理代码。
运行
go mod init命令,设置为新代码的模块路径。$ go mod init example/generics go: creating new go.mod: module example/generics注意: 对于生产环境的代码,你会指定一个更符合自己需求的模块路径。更多信息查看 Managing dependencies。
接下来,你会添加一些使用 Map 的简单代码。
添加非泛型函数
该步骤中,你会添加两个函数,它们都会把 Map 中的值相加,然后返回求和的结果。
要声明两个函数而不一个是因为你是在使用两种不同类型的映射:一个存储 int64 的值,一个存储 float64 的值。
编写代码
-
使用你的文本编辑器,在 generics 目录下创建一个名为 main.go 的文件。你会在该文件中编写 Go 代码。
-
进入 main.go ,在文件的最上方,粘贴下面的包声明。
package main一个独立的程序(相对的是库)总是在
main包中。 -
在包声明的下面,粘贴以下两个函数声明。
// SumInts 将 m 中的值加在一起。 func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } // SumFloats 将 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 。
-
-
在 main.go 的最上面,包声明以下,粘贴以下
main函数来初始化两个 map ,然后作为调用处理阶段声明的函数的参数。func main() { // 初始化用于整数值的 map ints := map[string]int64{ "first": 34, "second": 12, } // 初始化用于浮点数值的 map 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 的顶部附近,紧靠着包声明下方,导入支持刚才编写的代码所需的库。
代码的前几行看上去应该如下:
package main import "fmt" -
保存 main.go 。
运行代码
在包含 main.go 的目录的命令行,执行代码。
$ go run .
Non-Generic Sums: 46 and 62.97
使用泛型,在这里可以写一个函数代替两个。接着,你会添加一个泛型函数,它可用于包含整数值或浮点数值的 map 。
添加泛型函数处理多种类型
在该部分,你会添加单个泛型函数,它能接收包含整数值或浮点数值的 map ,可以高效地用一个函数替换上面刚刚编写的两个函数。
要支持其中某个类型的值,单个函数需要一个方式来声明它支持的类型。调用代码,另一方面,会需要明确是调用带整数值的 map 还是调用带浮点数值的 map 。
要支持这一点,你会编写一个函数,它还声明了 类型参数 作为原始的函数参数的补充。这些 类型参数 使函数泛型化,使其可以处理不同类型的参数。你会使用 类型参数 和原来的函数参数来调用函数。
每种类型参数都有一个 <类型约束> ,它作为类型参数的一种元类型。每个类型约束指定了允许的类型参数,这样调用的代码可以用于对应的类型参数。
一个类型参数的约束通常代表类型的集合,但在编译时类型参数代表了单个类型 - 作为调用代码时提供的类型参数。如果类型参数的类型不符合类型参数的约束,代码不会被编译。
请记住,一个类型参数必须支持对其执行的泛型代码的所有操作。例如,如果函数的代码尝试对约束包含数值类型的参数进行 string 操作(例如取索引),代码不会被编译。
在你要编写的代码中,你可以使用允许整数和浮点数类型的约束。
编写代码
-
在前面添加的两个函数下面,粘贴以下泛型函数。
// SumIntsOrFloats 对 Map m 时的值求和。它同时支持 int64 和 float64 作为用于 map 值的类型。 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。专门用于以下几种情况,在 go 中comparable已经预定义。它允许使用值作为比较操作符==和!=的运算对象的任意类型。Go 需要 Map 的键值是可比较的。所以声明K作为comparable是必要的,这样你可以使用K作为 map 变量的键。它也确保调用的代码能为 map 键使用允许的类型。 - 为
V类型参数指定两种类型合集的约束:int64和float64。 使用|指定两种类型的合集,这意味着该约束允许其中的任一种类型。在调用的代码中其中的任意一种类型都被编译器允许作为一个参数。 - 指定
map[K]V类型的参数m,这里K和V是已经为类型参数指定的类型。注意我们知道map[K]V是有效的映射类型,因为是可比较的类型。如果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 对 map m 中的值求和。它同时支持整数和浮点数作为 map 的值。 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,打印每个值的和。正如前面部分的内容,在调用泛型函数时省略了类型参数(中括号中的类型名)。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 Tour 是一个循序渐进的 Go 原理的说明。
- 你会在 Effective Go 和 How to write Go code 的描述中发现有用的 Go 的最佳实践。
完整代码
可以在Go playground 中运行代码。 在 playground 中只需简单地点击 运行 按钮。
package main
import "fmt"
type Number interface {
int64 | float64
}
func main() {
// 初始化整数值的 map
ints := map[string]int64{
"first": 34,
"second": 12,
}
// 初始化浮点数值的 map
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 将 m 中的值求和。
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats 将 m 中的值求和。
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
// SumIntsOrFloats 将 map 同中的值求和。它同时支持整数和浮点数作为 map 的值。
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
// SumNumbers 将 map 同中的值求和。它同时支持整数和浮点数作为 map 的值。
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}