Go 泛型预览

即将发布的 Go 1.18 将会带来一个巨大的更新:泛型。

官方教程:https://go.dev/doc/tutorial/generics

这篇文章是对官方指引的翻译。

准备

在正式尝试泛型之前,需要做如下的工作

  1. 安装 go1.18beta1 或更新的版本。安装方式请看 beta版本的安装与使用
  2. 一个可以编辑代码的编辑器,任何你喜欢的编辑器都可以
  3. 命令行终端,在 Linux 和 mac 上可以使用自带的 terminal,在 Windows 上可以使用 powershell 或者 cmd

安装并使用测试版 Go 工具链

由于 Go1.18 还未正式发布,因此我们需要安装测试版工具链来尝鲜,按照如下的步骤来安装:

  1. 运行下面的命令来安装测试版

    1
    $ go install golang.org/dl/go1.18beta2@latest
  2. 运行下面的命令来下载更新

    1
    $ go1.18beta2 download
  3. 使用测试版本程序取代原有的 go 命令(如果安装了正式版的 go)

    可以直接使用测试版的名字,或者将 go设置为测试版程序的别名

    • 直接使用测试版,你可以运行下面的命令来调用测试版程序来取代 go 命令:

      1
      $ go1.18beta2 version
    • go设置为别名,可以让测试版的使用更加简单

      1
      2
      $ alias go=go1.18beta2
      $ go version

之后的命令将会默认你已经设置了别名

创建一个代码文件夹

首先需要一个文件夹来存放测试代码

  1. 打开命令行,进入家目录

    在 Linux 或者 Mac 上

    1
    $ cd

    在 Windows 上

    1
    C:\> cd %HOMEPATH%

    教程的其余部分将显示$作为提示。您使用的命令也可以在Windows上使用。

  2. 在命令行行中创建一个目录

    1
    2
    $ mkdir generics
    $ cd generics
  3. 创建一个新的 Go module 来存放代码

    运行go mod init命令,并给定模块的路径

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

    注意: 对于生产代码, 你需要指定一个与你的需求更加契合的模块路径。更多信息可以参考 Managing dependencies.

接下来,你将会写一些简单的代码来接触泛型

新增一个非泛型函数

在这一步,你将会增加两个函数,每个函数都会将 map 中的值相加,然后返回最终的和。

之所以会定义两个函数,是因为有两种不同类型的 map 需要被处理:一种的值类型为 int64,另一种的值类型则是 float64.

编写代码

  1. 用你的编辑器在目录下创建一个 main.go文件,你将会在这个文件中创建代码

  2. 在 main.go 文件的最顶部,粘贴下面的包声明语句

    1
    package main

    一个独立的可执行程序(而不是库)入口总是在 main 包中。

  3. 在包声明语句下方粘贴下面的函数代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 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
    }

    在上述的代码中,你做了如下的事:

    • 定义了两个函数,每个函数都会将 map 中的值相加,然后返回最终的和。
      • SumInts 处理 string-int64 类型的 map
      • SumFloats 处理 string-float64 类型的 map
  4. 在 main.go 中,包声明语句的下方粘贴 main 方法的代码。在 main 方法中,会初始化两个 map,并且将它们作为参数传入在上一步中定义的方法内。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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))
    }

    在上面的代码中,做了如下的事情:

    • 初始化了值类型为 float64 的 map 和值类型为 int64 的 map,每个 map 都有两个元素
    • 调用上一步定义的函数来获取每个 map 中的值的和
    • 将结果打印出来
  5. 在 main.go 中,就在包定义语句的下方,导入刚刚书写的代码中所需要的包。

    第一行代码应该如下所示:

    1
    2
    3
    pacakge main

    import "fmt"
  6. 保存 main.go

运行代码

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

1
2
$ go run .
Non-Generic Sums: 46 and 62.97

通过使用泛型,你可以使用一个函数来完成需求。在接下来的步骤中,将会添加一个函数来处理值类型为 int64 或 float64 的 map。

新增一个泛型函数来处理多种类型

在这一节,你将会添加一个泛型函数,它接受值类型为 float64 或 int64 的 map 作为参数,使用单个函数就可以高效的替换原有的两个函数。

为了支持任一类型的值,这个函数需要一种方法来声明它支持的类型。另一方面,函数的调用者需要一种方法来指定传入的 map 是float64 还是 int64 的。

为了支持这一点,你将编写一个函数,除了普通的函数参数外,还会声明类型参数。这些类型参数使函数具有通用性(泛型),使其能够处理不同类型的参数。你将使用类型参数和普通函数参数调用函数。

每个参数类型都有着一个类型约束type constraint ),作为参数类型的元类型(meta-type)。每个类型约束都指定了,在调用函数时所允许传入的参数类型。

虽然类型约束通常代表了一组类型,但在编译时,类型参数代表了单个类型——函数调用者提供的参数类型的类型。如果类型参数的类型不符合类型约束,那么代码将无法编译。

请记住,类型参数必须支持泛型代码在该类型上执行的所有操作。例如,如果函数的代码试图对约束包含 int 类型参数执行 string 操作(如 strings.Index ),那么代码将无法通过编译。

在即将编写的代码中,你将会使用允许 int 以及 float 的类型约束。

编写代码

  1. 在上一节新增的代码下方,粘贴泛型函数的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // SumIntsOrFloats sums the values of map m. It supports both int64 and float64
    // as types for 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
    }

    在这段代码中:

    • 定义了 SumIntsOrFloats 函数,该函数包含了两个类型参数(在方括号中)K 和 V,以及一个类型为 map[K]V 的参数 m。函数的返回值类型为 V
    • 指定 K 的类型约束为 comparable。 comparable 是 go 预设的一个类型约束。它允许任何满足约束的类型的值都可以用作比较运算符 == 和 !=。这是因为在 Go 中 map 的 key 的类型必须是可比较的。因此将 K 设为 comparable 就是非常必要的了,你可以使用 K 来作为 map 的key。同时保证了调用者只使用被允许的类型的 key。
    • 指定 V 的类型约束为 int64 和 float64 两种类型的联合类型。使用操作符|指定两种类型的联合类型,表示该约束允许其中的任意一种类型。编译器允许这两种类型作为参数的类型传入函数。
    • 指定了 m 的类型为 map[K]V,K,V 的类型已经被类型参数所指定了。需要注意的是, map[K]V 是一个合法的 map 类型,因为K 已经被声明为 comparable 了。如果没有将 K 声明为 comparable,编译器将拒绝 map[K]V 的使用
  2. 在 main.go 中,粘贴下面的代码到原有代码的下方

    1
    2
    3
    fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

    在这段代码中:

    • 调用了刚才定义的泛型函数,传入了创建的 map

    • 指定了类型参数 – 包含在方括号中的类型的名称 - 表明在调用时需要被替换的类型参数的类型

      正如下一节中看到的,通常可以在函数调用中省略类型参数。Go编译器通常可以从你的代码中推断它们。

    • 打印函数的返回值

运行代码

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

1
2
3
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97

要运行代码,在每次调用中,编译器都会将类型参数替换为该调用中指定的具体类型。

在调用泛型函数时,通过指定类型参数,来告诉编译器使用哪些类型来代替函数的类型参数。

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

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

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

请注意,这并不总是可行的。例如,如果在需要调用没有参数的泛型函数时,需要在函数调用中包含类型参数。

编写代码

  • 在 main.go 中,粘贴下面的代码到原有代码的下方

    1
    2
    3
    fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

    在这段代码中:

    • 调用了泛型函数,并省略了类型参数

运行代码

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

1
2
3
4
$ 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 中,这样的话就可以在不同的地方重用它了。以这种方式声明约束有助于简化代码,例如当约束更复杂时。

你可以将类型约束定义为一个 interface。约束允许任何类型去实现接口。例如,声明了一个具有三种方法的类型约束接口,然后将其与泛型函数中的类型参数一起使用,则用于调用函数的类型参数必须具有所有的这些方法。

约束接口也可以引用特定类型,您将在本节中看到。

编写代码

  1. 就在 main 函数的上方,在导入语句之后,粘贴以下代码以声明类型约束。

    1
    2
    3
    type Number interface {
    int64 | float64
    }

    在这段代码中:

    • 声明要用作类型约束的Number接口类型。

    • 在接口内部声明了 int64 | float64 的联合类型。

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

  2. 在原有函数的下方,粘贴下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // SumNumbers sums the values of map m. It 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
    }

    在这段代码中:

    • 以与之前声明的泛型函数相同的逻辑声明泛型函数,但使用新的接口类型而不是联合作为类型约束。和之前前一样,使用参数和返回类型的类型参数
  3. 在 main.go 中,原有代码的下方,粘贴下列代码:

    1
    2
    3
    fmt.Printf("Generic Sums with Constraint: %v and %v\n",
    SumNumbers(ints),
    SumNumbers(floats))

    在这段代码中:

    • 对每个 map 调用 SumNumbers,并打印每个结果。

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

运行代码

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

1
2
3
4
5
$ 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

完整代码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
}

Go 泛型预览
https://blog.zhangliangliang.cc/post/go-generics.html
作者
Bobby Zhang
发布于
2022年2月17日
许可协议