开始使用泛型
文章目录
一、说明
这篇文章介绍Go中基本的泛型。使用泛型,你能声明和使用函数或者类型,它们被写用来和被调用者代码提供的任意集合类型一起工作。
在这篇指导中,你将声明两个简单的非泛型函数,然后在单个泛型函数中获取一些逻辑。
你将通过下面的部分进行处理:
- 为你的代码创建一个目录;
- 添加非泛型函数;
- 添加泛型函数去处理多种数据类型;
- 当调用泛型函数的时候,移除类型参数;
- 声明一个类型的约束;
二、准备
- 安装Go1.18 及以上版本;
- 一个用于编写你的代码的工具;
- 一个命令行终端;
三、为你的代码创建一个目录
为了开始,创建一个目录为你将要写的代码。
-
打开命令行终端,切换到主目录
$ cd $
-
通过命令行,为你的代码创建一个目录,叫做
generics
$ cd generics/ $ cd generics $
-
创建一个模块用于持有你的代码
$ go mod init example/generics go: creating new go.mod: module example/generics $
注意,对于生产代码,你会指定一个更符合你自己需求的模块路径。
接下来,你会添加一些代码,用于处理maps。
四、添加非泛型函数
在这一步中,您将添加两个函数,每个函数将map的值相加并返回总数。
你将声明两个函数,而不是一个,因为你将使用两种不同的数据类型去工作:一个存储int64
值,一个存储float64
值。
4.1 写代码
-
使用你的编译器,在
generics
文件夹中创建一个main.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
获取一个map: String到float64 的值;SumInts
获取一个map: String到int64
的值;
- 声明了两个函数,为了将map中的值添加到一块,并返回和:
-
在
main.go
文件的顶部,包声明的下面,粘贴下面的main函数,去初始化两个map,并使用它们作为参数,调用你在前面步骤中声明的函数func main() { // 为Int 值初始化一个map ints := map[string]int64 { "first" : 34, "second" : 21, } // 为float值 初始化一个map floats := map[string]float64 { "first" : 35.98, "second" : 26.99, } fmt.Printf("非泛型 Sums:%v and %v\n", SumInts(ints), SumFloats(floats)) }
在这个代码中:
- 初始化一个
float64
和一个int64的map,每一个都有两项; - 调用你声明的两个函数,查询每一个结果的值;
- 打印结果;
- 初始化一个
-
在靠近
main.go
的顶部,仅仅在包声明的下面,导入需要支持你刚刚写的代码的包第一行代码看起来像这样:
package main import "fmt"
-
保存
main.go
文件
4.2 运行代码
在包含main.go
文件的目录的命令行,运行代码:
$ go run .
非泛型 Sums:55 and 62.97
$
使用泛型,你可以写一个函数代替两个。接下来,你将添加一个泛型函数,map既可以是float,又可以是integer。
五、添加泛型函数,用于处理多类型
在这一部分,你将添加一个单个泛型函数,它可以接收包含integer或float值的map。有效的使用单个函数替换了你刚刚写的两个函数。
为了支持两种数据类型,单个函数需要有一种方法声明它支持的数据类型。调用代码,另一个方面,将需要一种方法明确它正在调用一个integer的map或一个float的map。
为了支持这一点,您将编写一个除了普通函数参数之外还声明类型参数的函数。这种类型参数让函数泛型化,使其能够处理不同类型的参数。你将使用类型参数和普通的函数参数来调用函数。
每个类型参数都有一个类型的约束。充当类型参数的元类型。每一个类型约束指明了允许的类型参数,调用代码能够使用,对于各自的类型参数。
尽管一个类型参数的约束通常代表着一个类型集合。在编译的时候,编译时类型参数代表单一类型——类型被提供做为被调用代码的类型参数。如果类型参数的约束不允许类型参数的类型,代码将不能编译。
记住,类型参数必须支持泛型代码对其执行的所有操作。例如,如果你的函数试着在类型参数上进行string操作(例如索引),它如果包含数值类型,代码将不能编译。
在你将写的代码中,你将使用一个约束,即允许integer类型,又允许float类型。
5.1 写代码
-
在上一部分你添加的两个函数下面,粘贴下面的泛型函数。
// SumIntsOrFloat 获取m值的和。它既支持int64类型,也支持float64类型 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作为key在map变量中。它还确保调用代码对映射键使用允许的类型。 - 指定
V
类型参数一个约束,是两种类型的并集:int64 和 float64。使用|
指明两种类型的并集。意味着这种约束允许两种类型的任意个。编译器将允许任一类型作为调用代码中的参数。 - 指定了一个m参数,它的类型是
map[K]V
,K和V就是指定的类型参数。注意,我们知道map[K]V
是一个有效的map类型,因为K是一个可比较类型。如果我们没有声明K为comparable
,编译器将拒绝指向map[K]V
的引用。
- 声明了一个
-
在
main.go
文中,在你已经写的代码下面,粘贴下面的代码fmt.Printf("泛型求和: %v 和 %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats))
在这个代码中:
-
调用泛型函数,传递你创造的map;
-
指定了类型参数:类型的名称在中括号里。清楚在你要调用的函数中,要替换的类型参数。
就像你在下部分将看到的,在函数调用中,你通常省略类型参数。Go能够从你的代码中推断。
-
打印从函数返回的和。
-
5.2 运行代码
在包含main.go
文件目录的命令行下,运行代码:
$ go run .
非泛型 Sums:55 and 62.97
泛型求和: 55 和 62.97
$
为你运行你的代码,在每次调用中,编译器将使用调用中的具体的类型替换类型参数。
在调用你写的泛型函数时,你指定了类型参数。它告诉编译器,什么类型将替换函数的类型参数。正如你在下部分看到的,很多场景下,你能忽略这些类型参数,因为编译器能推断它们。
六、在调用泛型函数的时候,移除类型参数
在这一部分,你将添加一个修改版本的泛型参数调用,制造一个聪明的改变去简化调用代码。你将移除类型参数,它们在这些场景下不需要。
当Go编译器能够推断你使用的类型参数时,你可以忽略类型参数。编译器从函数参数类型推断类型参数。
注意,它并不是总是可能的。例如,如果你需要调用没有参数泛型函数。你在函数调用的时候,需要包含类型参数。
6.1 写代码
-
在
main.go
文件中,在上面的代码下面,粘贴下面的代码:fmt.Printf("泛型求和,推断类型参数: %v 和 %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats))
在这个代码中:
- 调用泛型函数,省略了类型参数。
6.2 运行代码
在包含main.go
文件的目录下,运行下面的代码:
$ go run .
非泛型 Sums:55 and 62.97
泛型求和: 55 和 62.97
泛型求和,推断类型参数: 55 和 62.9
$
七、定义类型约束
在最后一部分,你将移动你早期定义的约束到你自己的接口中,以便于你重复使用它们在多个地方。以这种方式声明约束有助于简化代码,例如当约束更复杂时。
你能声明一个类型约束作为一个接口。这个约束允许任何类型实现接口。例如,如果你声明一个类型约束接口,带有三个方法。那么使用它,在一个带有一个类型参数的泛型函数中,用于调用函数的类型参数必须具有所有这些方法。
约束接口也能指向特定的类型,就像你在这部分看到的。
7.1 写代码
-
在
main
函数上面,导入语句下面,粘贴下面的类型参数去声明一个类型约束。type Number interface { int64 | float64 }
在代码中:
-
声明了
Number
接口类型,用于作为类型约束; -
在接口的里面声明一个int64和float64的并集;
本质上,你是移动了并集从函数声明中,进入新的类型约束。这种方法,当你想要约束一个类型参数成为int64或float64,你能使用Number类型约束代替写
int64 | flat64
。
-
-
在你已经声明的函数下面,粘贴下面的泛型函数
// SumNumbers 对m的值求和。它既支持integer,又支持floats 作为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("泛型求和,使用约束: %v 和 %v\n", SumNumbers(ints), SumNumbers(floats))
在这个代码中:
-
对每一个map,调用
SumNumbers
,打印每个值就像上一部分,在调用泛型函数的时候省略了类型参数。编译器能够推断出类型参数。
-
7.2 运行代码
在包含main.go
文件目录的命令行下,运行代码:
$ go run .
非泛型 Sums:55 and 62.97
泛型求和: 55 和 62.97
泛型求和,推断类型参数: 55 和 62.97
泛型求和,使用约束: 55 和 62.97
$