注意:这是测试内容
本教程介绍了Go中泛型的基本知识。通过泛型,你可以声明和使用函数或类型,这些函数或类型被写成可以与调用代码提供的一系列类型中的任何一个一起工作。
在本教程中,你将声明两个简单的非泛型函数,然后在一个泛型函数中捕获相同的逻辑。
你将通过以下几个部分取得进展:
- 为你的代码创建一个文件夹。
- 添加非通用函数。
- 添加一个通用函数来处理多种类型。
- 在调用通用函数时删除类型参数。
- 声明一个类型约束。
注:关于其他教程,请参见教程。
注意:如果你愿意,你可以在 “Go dev branch “模式下使用Go playground来代替编辑和运行你的程序。
前提条件
- 安装Go 1.18 Beta 1或更高版本。 关于安装说明,请参阅安装和使用测试版。
- 一个编辑代码的工具。 你拥有的任何文本编辑器都可以工作。
- 一个命令终端。 在 Linux 和 Mac 上使用任何终端,以及在 Windows 上使用 PowerShell 或 cmd,Go 都能很好地工作。
安装和使用测试版
本教程需要Beta 1中的泛型功能。要安装该测试版,请遵循以下步骤。
1.运行以下命令来安装测试版。
1 | $ go install golang.org/dl/go1.18beta1@latest |
2.运行下面的命令来下载更新。
1 | $ go1.18beta1 download |
3.使用测试版而不是已发布的Go版本(如果你有的话)来运行Go命令。你可以通过使用测试版的名称或将测试版别名为其他名称来运行测试版的命令。
- 使用测试版的名称,你可以通过调用 go1.18beta1 而不是 go 来运行命令。
1
$ go1.18beta1 version
- 通过将测试版的名字别名为另一个名字,你可以简化命令。
1
2$ alias go=go1.18beta1
$ go version
本教程中的命令将假定你已经别名了测试版的名称。
为你的代码创建一个文件夹
首先,为你要写的代码创建一个文件夹。
1.打开一个命令提示符,改变到你的主目录。
Linux or Mac:
1 | $ cd |
Windows:
1 | $ C:\> cd %HOMEPATH% |
教程的其余部分将显示$作为提示。你使用的命令在Windows上也可以使用。
2.在命令提示符下,为你的代码创建一个名为泛型的目录。
1 | $ mkdir generics |
3.创建一个模块来存放你的代码。
运行go mod init命令,给它你的新代码的模块路径。
1 | $ go mod init example/generics |
注意: 对于生产代码,你应该指定一个更符合你自己需求的模块路径。更多信息,请务必参阅管理依赖。
接下来,你将添加一些简单的代码来处理map。
添加非泛型函数
在这一步中,你将添加两个函数,分别将一个map的值相加并返回总数。
你要声明两个函数而不是一个,因为你要处理两种不同类型的map:一个是存储int64值的,另一个是存储float64值的。
编写代码
1.使用你的文本编辑器,在 generics 目录中创建一个名为 main.go 的文件。你将在这个文件中编写你的Go代码。
2.在main.go中,在文件的顶部,粘贴以下软件包声明。
1 | package main |
一个独立的程序(相对于一个库)总是在包main中。
3.在包的声明下面,粘贴以下两个函数的声明。
1 | // SumInts adds together the values of m. |
在这段代码中,你:
- 声明两个函数,将一个map的值加在一起,并返回总和。
- SumFloats接收一个字符串到float64值的映射。
- SumInts接收一个从字符串到int64值的映射。
4.在main.go的顶部,在包的声明下面,粘贴以下main函数,以初始化两个map,并在调用你在上一步声明的函数时将它们作为参数。
1 | func main() { |
在这段代码中,你:
- 初始化一个float64值的映射和一个int64值的映射,每个映射有两个条目。
- 调用你先前声明的两个函数,以找到每个映射的值的总和。
- 打印结果。
5.在main.go的顶部,就在包声明的下面,导入你需要的包,以支持你刚刚写的代码。
第一行代码应该是这样的:
1 | package main |
6.保存main.go
运行代码
从包含main.go的目录中的命令行,运行代码。
1
2$ go run .
Non-Generic Sums: 46 and 62.97
有了泛型,你可以在这里写一个函数而不是两个。接下来,你将为包含整数或浮点数的map添加一个泛型函数。
增加一个泛型函数来处理多种类型
在这一节中,你将添加一个单一的通用函数,它可以接收包含整数或浮点数值的映射,有效地用一个单一的函数代替了你刚才写的两个函数。
为了支持这两种类型的值,这个单一的函数需要一种方法来声明它支持哪些类型。另一方面,调用代码将需要一种方法来指定它是用整数还是浮点图来调用。
为了支持这一点,你将编写一个除了普通函数参数之外还声明了类型参数的函数。这些类型参数使该函数具有通用性,使它能够与不同类型的参数一起工作。你将用类型参数和普通函数参数来调用该函数。
每个类型参数都有一个类型约束,作为类型参数的一种元类型。每个类型约束都指定了调用代码可以为各自的类型参数使用的允许的类型参数。
虽然一个类型参数的约束通常代表一组类型,但在编译时,类型参数代表一个单一的类型–由调用代码提供的类型参数。如果类型参数的类型不被类型参数的约束所允许,代码就不会被编译。
请记住,一个类型参数必须支持通用代码对它进行的所有操作。例如,如果你的函数的代码试图在一个类型参数上执行字符串操作(如索引),而这个类型参数的约束条件包括数字类型,那么代码就不会被编译。
在你要写的代码中,你将使用一个允许整数或浮点数类型的约束条件。
编写代码
1.在你之前添加的两个函数下面,粘贴以下通用函数。
1 | // SumIntsOrFloats sums the values of map m. It supports both int64 and float64 |
在这段代码中,你:
- 声明一个SumIntsOrFloats函数,有两个类型参数(在方括号内),K和V,以及一个使用类型参数的参数,map[K]V类型的m。该函数返回一个类型为V的值。
- 为K类型参数指定可比较的类型约束。可比约束是专门为类似这样的情况而设计的,在Go中预先声明了可比约束。它允许任何类型的值可以作为比较运算符==和!=的操作数。 Go要求映射键是可比较的。因此,将K声明为可比较是必要的,这样你就可以将K作为map变量的键。它还可以确保调用代码使用允许的类型作为map键。
- 为V类型参数指定一个约束,该约束是两种类型的联合:int64和float64。使用|指定了这两种类型的联合,意味着这个约束允许任何一种类型。任何一种类型都将被编译器允许作为调用代码中的一个参数。
- 指定m参数是map[K]V类型,其中K和V是已经为类型参数指定的类型。注意,我们知道map[K]V是一个有效的map类型,因为K是一个可比较的类型。如果我们没有声明K是可比较的,编译器会拒绝对map[K]V的引用。
2.在main.go中,在你已经有的代码下面,粘贴以下代码。
1 | fmt.Printf("Generic Sums: %v and %v\n", |
在这段代码中,你:
调用你刚才声明的通用函数,传递你创建的每一个映射。
指定类型参数–方括号中的类型名称–以明确在你所调用的函数中应该取代类型参数的类型。
正如你将在下一节看到的,你通常可以在函数调用中省略类型参数。Go通常可以从你的代码中推断出它们。
打印函数所返回的和。
运行代码
从包含main.go的目录中的命令行,运行代码。
1 | $ go run . |
为了运行你的代码,在每次调用中,编译器都用该调用中指定的具体类型替换类型参数。
在调用你写的泛型函数时,你指定了类型参数,告诉编译器用什么类型来代替函数的类型参数。正如你在下一节所看到的,在许多情况下,你可以省略这些类型参数,因为编译器可以推断出它们。
调用泛型函数时删除类型参数
在这一节中,你将添加一个修改过的泛型函数调用版本,做一个小改动以简化调用代码。你将删除类型参数,在这种情况下不需要这些参数。
当Go编译器可以推断出你想要使用的类型时,你可以在调用代码中省略类型参数。编译器会从函数参数的类型中推断出类型参数。
请注意,这并不总是可能的。例如,如果你需要调用一个没有参数的通用函数,你需要在函数调用中包含类型参数。
编写代码
- 在main.go中,在你已经有的代码下面,粘贴以下代码。在这个代码中,你:
1
2
3
4fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats)) - 调用通用函数,省略类型参数。
运行代码
从包含main.go的目录中的命令行,运行代码。
1 | $ go run . |
接下来,你将进一步简化这个函数,把整数和浮点数的结合捕捉到一个你可以重复使用的类型约束中,比如从其他代码中。
声明一个类型约束
在最后一节中,你将把你先前定义的约束移到它自己的接口中,这样你就可以在多个地方重复使用它。以这种方式声明约束有助于简化代码,例如当一个约束比较复杂时。
你把一个类型约束声明为一个接口。该约束允许任何实现该接口的类型。例如,如果你声明一个具有三种方法的类型约束接口,然后在一个通用函数中用一个类型参数来使用它,用于调用该函数的类型参数必须具有所有这些方法。
约束接口也可以指代特定的类型,正如你将在本节中看到的那样。
编写代码
1.就在main上面,紧接着import语句,粘贴以下代码来声明一个类型约束。
1 | type Number interface { |
在这段代码中,你:
声明Number接口类型,作为类型约束使用。
在接口内声明一个int64和float64的联合。
从本质上讲,你把联合从函数声明中移到一个新的类型约束中。这样,当你想把一个类型参数约束到int64或float64时,你可以使用这个Number类型约束而不是写出int64 | float64。
2.在你已经有的函数下面,粘贴以下通用的SumNumbers函数。
1 | // SumNumbers sums the values of map m. It supports both integers |
在这段代码中,你:
- 声明一个泛型函数,其逻辑与你之前声明的泛型函数相同,但用新的接口类型而不是联盟作为类型约束条件。和以前一样,你用类型参数来表示参数和返回类型。
3.在main.go中,在你已经有的代码下面,粘贴以下代码。
1 | fmt.Printf("Generic Sums with Constraint: %v and %v\n", |
在这段代码中,你:
- 用每个map调用SumNumbers,打印每个map的值的总和。 与上一节一样,在调用泛型函数时省略类型参数(方括号中的类型名称)。Go编译器可以从其他参数中推断出类型参数。
运行代码
从包含main.go的目录中的命令行,运行代码。
1 | $ go run . |
结论
做得很好! 你刚刚向自己介绍了Go中的泛型。
建议的下一个主题:
- Go Tour 是对 Go 基础知识的一个很好的逐步介绍。
- 你会在Effective Go和How to write Go code中找到有用的Go最佳实践。
完成的代码
你可以在Go playground上运行这个程序。在操场上只需点击运行按钮。
1 | package main |