Go语言泛型教程

这个教程介绍了Go中泛型的基础。使用泛型,你可以声明和使用函数或类型,这些函数或类型被编写为与调用代码提供的一组类型中的任何一个类型一起工作。

在这个教程中,你将声明两个简单的非泛型函数,然后将相同的逻辑捕获到一个单一的泛型函数中。

你将通过以下部分进行:

  1. 为你的代码创建一个文件夹。

  2. 添加非泛型函数。

  3. 添加一个泛型函数来处理多种类型。

  4. 在调用泛型函数时移除类型参数。

  5. 声明一个类型约束。

注意:
 对于其他教程,请看教程。

注意:
 如果你更喜欢,你可以使用“Go dev分支”模式下的Go游乐场来编辑和运行你的程序。

先决条件

  • 安装Go 1.18或更高版本。
     安装说明,请看安装Go。

  • 一个编辑代码的工具。
     任何文本编辑器都可以。

  • 一个命令终端。
     Go在Linux和Mac的任何终端上都很好用,在Windows的PowerShell或cmd上也很好用。

为你的代码创建一个文件夹

开始时,为你将编写的代码创建一个文件夹。

  1. 打开命令提示符并切换到你的主目录。

在Linux或Mac上:

1
2
3
4
$ cd
```

在Windows上:

C:> cd %HOMEPATH%

1
2
3
4
5
  
教程的其余部分将显示一个$
作为提示符。你使用的命令也将在Windows上工作。
1. 从命令提示符中,为你的代码创建一个名为generics的目录。

$ mkdir generics$ cd generics

1
2
3
4
1. 创建一个模块来保存你的代码。  

运行go mod init
命令,给它你的新代码的模块路径。

$ go mod init example/genericsgo: creating new go.mod: module example/generics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  
**注意:**
 对于生产代码,你会指定一个更具体的模块路径。更多信息,请确保查看管理依赖项。

接下来,你将添加一些简单的代码来处理映射。
## 添加非泛型函数

在这一步中,你将添加两个函数,每个函数都会将映射中的值相加并返回总和。

你声明两个函数而不是一个,因为你正在处理两种不同类型的映射:一个存储int64
值,另一个存储float64
值。
#### 编写代码
1. 使用文本编辑器,在generics目录中创建一个名为main.go的文件。你将在该文件中编写Go代码。

1. 在main.go中,在文件顶部粘贴以下包声明。

package main

1
2
3
4
5
  
作为一个独立的程序(与库相对),总是在main
包中。
1. 在包声明下方,粘贴以下两个函数声明。

// 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}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  
在这段代码中,你:
- 声明两个函数来将映射的值相加并返回总和。

- SumFloats
接受一个string
float64
值的映射。

- SumInts
接受一个string
int64
值的映射。

1. 在main.go的顶部,包声明下方,粘贴以下main
函数来初始化两个映射,并在调用你之前声明的函数时将它们作为参数。

func main() {    // 初始化一个整数值的映射    ints := map[string]int64{        “first”: 34,        “second”: 12,    }    // 初始化一个浮点值的映射    floats := map[string]float64{        “first”: 35.98,        “second”: 26.99,    }    fmt.Printf(“Non-Generic Sums: %v 和 %v\n”,        SumInts(ints),        SumFloats(floats))}

1
2
3
4
5
6
7
8
9
10
11
12
13
  
在这段代码中,你:
- 初始化一个float64
值的映射和一个int64
值的映射,每个都有两个条目。

- 调用你之前声明的两个函数来找到每个映射值的总和。

- 打印结果。

1. 在main.go的顶部,就在包声明下方,导入你刚刚编写的代码所需的包。

代码的前几行应该看起来像这样:

package mainimport “fmt”

1
2
3
4
5
1. 保存main.go。  

#### 运行代码

在包含main.go的目录中,从命令行运行代码。

$ go run .Non-Generic Sums: 46 和 62.97

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  
使用泛型,你可以在这里用一个函数代替两个。接下来,你将添加一个单一的泛型函数来处理包含整数或浮点值的映射。
## 添加一个泛型函数来处理多种类型

在这一节中,你将添加一个单一的泛型函数,它可以接收一个包含整数或浮点值的映射,有效地用一个函数替换了你刚刚编写的两个函数。

为了支持两种类型的值,那个单一的函数将需要一种方式来声明它支持的类型。另一方面,调用代码将需要一种方式来指定它是用整数映射还是浮点映射来调用的。

为了支持这一点,你将编写一个声明了类型参数的函数,除了它的普通函数参数之外。这些类型参数使函数泛型,使其能够使用不同类型的参数。你将用类型参数和普通函数参数来调用函数。

每个类型参数都有一个类型约束,它作为一种元类型为类型参数。每个类型约束指定了调用代码可以为相应类型参数使用的允许类型参数。

虽然类型参数的约束通常代表一组类型,但在编译时类型参数代表一个单一的类型——调用代码作为类型参数提供的类型。如果类型参数的类型不被类型参数的约束所允许,代码将不会编译。

请记住,类型参数必须支持泛型代码对它执行的所有操作。例如,如果你的函数代码试图对类型参数执行string
操作(例如索引),而类型参数的约束包括了数值类型,代码将不会编译。

在你即将编写的代码中,你将使用一个允许整数或浮点类型的约束。
#### 编写代码
1. 在你之前添加的两个函数下方,粘贴以下泛型函数。

// SumIntsOrFloats将映射m的值相加。它支持int64和float64作为映射值的类型。func SumIntsOrFloatsK comparable, V int64 | float64 V {    var s V    for _, v := range m {        s += v    }    return s}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  
在这段代码中,你:
- 声明了一个SumIntsOrFloats
函数,它有两个类型参数(在方括号内),K
V
,以及一个使用类型参数的参数,m
类型为map[K]V
。函数返回一个类型为V
的值。

-K
类型参数指定了类型约束comparable
。这个comparable
约束是Go中预声明的,专门用于这种情况。它允许任何可以作为比较运算符==
!=
的操作数的类型。Go要求映射的键是可比较的。因此,声明K
comparable
是必要的,这样你就可以在映射变量中使用K
作为键。它还确保调用代码使用了允许的映射键类型。

-V
类型参数指定了一个约束,这是一个包含两种类型的联合:int64
float64
。使用|
指定了这两种类型的联合,意味着这个约束允许任一类型。编译器将允许任一类型作为调用代码中的参数。

- 指定m
参数的类型为map[K]V
,其中K
V
是为类型参数指定的类型。注意,我们知道map[K]V
是一个有效的映射类型,因为K
是一个可比较的类型。如果我们没有声明K
为可比较的,编译器将拒绝对map[K]V
的引用。

1.main.go中,你已有的代码下方,粘贴以下代码。

fmt.Printf(“Generic Sums: %v 和 %v\n”,    SumIntsOrFloatsstring, int64,    SumIntsOrFloatsstring, float64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  
在这段代码中,你:
- 调用你刚刚声明的泛型函数,传递你创建的每个映射。

- 指定类型参数——方括号中的类型名称——以明确你希望在调用的函数中替换类型参数的类型。

正如你将在下一节中看到的,你可以在很多情况下省略这些类型参数,

因为编译器可以从你的代码中推断它们。

- 打印函数返回的总和。

#### 运行代码

在包含main.go的目录中,从命令行运行代码。

$ go run .Non-Generic Sums: 46 和 62.97Generic Sums: 46 和 62.97

1
2
3
4
5
6
7
8
9
10
11
12
  
在调用你编写的泛型函数时,你指定了类型参数,告诉编译器在函数的类型参数中使用什么类型。正如你将在下一节中看到的,很多时候你可以省略这些类型参数,因为编译器可以从你的代码中推断它们。
## 在调用泛型函数时移除类型参数

在这一节中,你将添加泛型函数调用的修改版本,进行一个小的更改以简化调用代码。你将移除类型参数,这些在这种情况下是不需要的。

你可以在调用代码中省略类型参数,当Go编译器可以从你的代码中推断出你想使用的类型时。编译器从函数参数的类型中推断类型参数。

注意,这并不总是可能的。例如,如果你需要调用一个没有任何参数的泛型函数,你将需要在函数调用中包含类型参数。
#### 编写代码
- 在main.go中,你已有的代码下方,粘贴以下代码。

fmt.Printf(“Generic Sums, type parameters inferred: %v 和 %v\n”,    SumIntsOrFloats(ints),    SumIntsOrFloats(floats))

1
2
3
4
5
6
7
  
在这段代码中,你:
- 调用泛型函数,省略类型参数。

#### 运行代码

在包含main.go的目录中,从命令行运行代码。

$ go run .Non-Generic Sums: 46 和 62.97Generic Sums: 46 和 62.97Generic Sums, type parameters inferred: 46 和 62.97

1
2
3
4
5
6
7
8
9
10
11
12
13
  
接下来,你将进一步简化函数,通过将整数和浮点数的联合捕获到一个你可以重用的类型约束中,例如从其他代码中。
## 声明一个类型约束

在这最后一节中,你将把你之前定义的约束移动到它自己的接口中,以便你可以在多个地方重用它。以这种方式声明约束有助于简化代码,例如当约束更复杂时。

你将声明一个类型约束作为接口。约束允许任何实现了接口的类型。例如,如果你声明了一个具有三个方法的类型约束接口,然后使用它与泛型函数中的类型参数,用于调用函数的类型参数必须具有所有这些方法。

约束接口也可以引用特定类型,正如你将在这一节中看到的。
#### 编写代码
1. 就在main
上方,在导入语句之后,粘贴以下代码以声明一个类型约束。

type Number interface {    int64 | float64}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  
在这段代码中,你:
- 声明Number
接口类型用作类型约束。

- 在接口内声明了一个int64
float64
的联合。

本质上,你是将联合从函数声明移动到一个新的类型约束中。这样,当你想要限制一个类型参数为int64
float64
时,你可以使用这个Number
类型约束,而不是写出int64 | float64


1. 在你已有的函数下方,粘贴以下泛型SumNumbers
函数。

// SumNumbers将映射m的值相加。它支持整数和浮点数作为映射值。func SumNumbersK comparable, V Number V {    var s V    for _, v := range m {        s += v    }    return s}

1
2
3
4
5
6
  
在这段代码中,你:
- 声明了一个泛型函数,逻辑与之前声明的泛型函数相同,但使用新的接口类型代替了联合作为类型约束。像以前一样,你使用类型参数作为参数和返回类型。

1. 在main.go中,你已有的代码下方,粘贴以下代码。

fmt.Printf(“Generic Sums with Constraint: %v 和 %v\n”,    SumNumbers(ints),    SumNumbers(floats))

1
2
3
4
5
6
7
8
9
10
  
在这段代码中,你:
- 用每个映射调用SumNumbers
,打印每个的值的总和。

像前一节一样,你在调用泛型函数时省略了类型参数(方括号中的类型名称)。Go编译器可以从其他参数中推断出类型参数。

#### 运行代码

在包含main.go的目录中,从命令行运行代码。

$ go run .Non-Generic Sums: 46 和 62.97Generic Sums: 46 和 62.97Generic Sums, type parameters inferred: 46 和 62.97Generic Sums with Constraint: 46 和 62.97

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 结论  

干得好!你刚刚介绍了Go中的泛型。

建议的下一个主题:
- Go Tour是Go基础知识的很好的逐步介绍。

- 你可以在Effective Go和如何编写Go代码中找到有用的Go最佳实践。

## 完成的代码

你可以在Go 
playground
中运行这个程序。只需点击**运行**
按钮。

package mainimport"fmt"type Number interface {    int64 | float64}func main() {    // 初始化一个整数值的映射    ints := map[string]int64{        “first”: 34,        “second”: 12,    }    // 初始化一个浮点值的映射    floats := map[string]float64{        “first”: 35.98,        “second”: 26.99,    }    fmt.Printf(“Non-Generic Sums: %v 和 %v\n”,        SumInts(ints),        SumFloats(floats))    fmt.Printf(“Generic Sums: %v 和 %v\n”,        SumIntsOrFloatsstring, int64,        SumIntsOrFloatsstring, float64)    fmt.Printf(“Generic Sums, type parameters inferred: %v 和 %v\n”,        SumIntsOrFloats(ints),        SumIntsOrFloats(floats))    fmt.Printf(“Generic Sums with Constraint: %v 和 %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将映射m的值相加。它支持floats和integers作为映射值。func SumIntsOrFloatsK comparable, V int64 | float64 V {    var s V    for _, v := range m {        s += v    }    return s}// SumNumbers将映射m的值相加。它支持整数和浮点数作为映射值。func SumNumbersK comparable, V Number V {    var s V    for _, v := range m {        s += v    }    return s}

  
  

![江达小记](/images/wechatmpscan.png)