Go 泛型随着 Go 1.17 版本来了,这篇文章是翻译自 Generics in Go
Go 泛型来了! 这是多年来 Go 语言最令人激动和巨大的变化之一。本教程用简单的语言解释了这一部分内容:
- 什么是泛型
- 为什么我们需要泛型
- 泛型在Go中如何工作
- 以及我们可以在哪里使用泛型。
它既简单又有趣,让我们一起跳舞吧!
什么是泛型?
如你所知,Go 是一种类型化的语言,这意味着你程序中的每个变量和值都有一些特定的类型,如 int
或 string
。当我们写函数时,我们需要在所谓的函数签名中指定其参数的类型,就像这样:
func PrintString(s string) {
这里,参数 s 是字符串类型的。我们可以想象写出这个函数的类似版本,接受 int
、float64
、任意结构类型等等。但这对于少数特定类型来说是不方便的,尽管我们有时可以使用接口来解决这个问题(例如在 map[string] interface 教程中描述的),但这种方法有很多限制。
Go 中的通用函数
相反,我们想声明一个通用函数--我们叫它 PrintAnything
--它接收一个任意类型的参数(我们叫它 T
),并对它做一些事情。
下面是它的样子。
func PrintAnything[T any](thing T) { fmt.Println(thing)}
很简单,对吗?any
表示 T
可以是任何类型。我们是说,对于任何类型的T
,PrintAnything
需要一个T
型的参数。
我们如何调用这样的函数?同样简单:
PrintAnything("Hello!")PrintAnything(42)PrintAnything(true)
无论我们调用函数时参数的类型是什么,这就是参数事物的类型。
限制条件
实现 PrintAnything
函数非常容易,因为 fmt 库反正可以打印任何东西。假设我们想写一个我们自己的版本,比如strings.Join
,它接收一个 T
的切片,并返回一个单一的字符串,将它们全部连接在一起。让我们试试吧。
// I have a bad feeling about this.func Join[T any](things []T) (result string) { for _, v := range things { result += v.String() } return result}
我们已经创建了一个通用函数Join()
,对于一个任意类型的 T
,它接受一个参数,这个参数是 T
的一个切片。
output := Join([]string{"a", "b", "c"})// v.String undefined (type bound for T has no method String)
问题是,在 Join()
函数中,我们想对每个切片元素 v
调用 .String()
,把它变成一个字符串。但是 Go 需要提前检查类型 T
是否有 String()
方法,由于它不知道 T
是什么,所以它无法做到这一点。
我们需要做的是稍微限制一下 T
的类型。我们不接受任何类型的 T
,而只对有 String()
方法的类型感兴趣。任何这样的类型都是我们的 Join()
函数可以接受的输入,那么我们如何在 Go 中表达这种约束呢?我们使用一个接口。
type Stringer interface { String() string}
这指定了一个给定的类型有一个 String()
方法。所以现在我们可以将这个约束应用于我们的泛型函数的类型。
func Join[T Stringer] ...
由于 Stringer
保证任何 T
类型的值都有一个 String()
方法,Go 现在会很乐意让我们在函数中调用它。但是如果你试图用一个不符合 Stringer 的类型的切片(例如 int )来调用 Join()
函数,Go会报错。
result := Join([]int{1, 2, 3})// int does not satisfy Stringer (missing method String)
comparable
限制
基于方法的约束(如 Stringer
)很有用,但是如果我们想对我们的通用输入做一些不涉及调用方法的事情怎么办?
例如,假设我们要编写一个 Equal
函数,它接受两个 T
类型的参数,如果它们相等则返回 true,否则返回 false。让我们试一试:
// This won't work.func Equal[T any](a, b T) bool { return a == b}fmt.Println(Equal(1, 1))// cannot compare a == b (operator == not defined for T)
这和我们在 Join()
中的 String()
方法的问题是一样的,但是由于我们现在没有调用方法,所以我们不能使用基于方法集的约束。相反,我们需要将 T
限制在只能使用 ==
或 !=
运算符的类型上,这些类型被称为可比较类型。幸运的是,有一个直接的方法来指定这一点:使用内置的可比较约束,而不是任何。
func Equal[T comparable] ...
constraints
包
为了方便起见,假设我们想对 T
的值做一些事情,而不是对它们进行比较或调用方法。例如,假设我们想为一个通用类型 T
写一个 Max()
函数,该函数接收 T
的一个片断并返回具有最高值的元素。我们可以试试这样的方法。
// Nope.func Max[T any](input []T) (max T) { for _, v := range input { if v > max { max = v } } return max}
我对此不太乐观,但让我们看看会发生什么:
fmt.Println(Max([]int{1, 2, 3}))// cannot compare v > max (operator > not defined for T)
同样,Go 无法提前证明类型 T
可以与 >
运算符一起使用(也就是说, T
是有序的)。我们如何解决这个问题?我们可以简单地列出约束中所有可能的允许类型,如下所示(称为类型列表):
type Ordered interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string}
对你的键盘来说幸运的是,这个和其他有用的约束已经在标准库的 constraints 包中为我们定义了,所以我们可以导入它并像这样使用它:
func Max[T constraints.Ordered] ...
问题解决了!
通用类型
到目前为止,太酷了。我们知道如何编写可以接受任何类型参数的函数。但是如果我们想创建一个可以包含任何类型的类型呢?例如,“任何东西的切片”类型。事实证明这很容易:
type Bunch[T any] []T
我们是说对于任何给定的类型 T
,Bunch[T]
是 T
类型值的切片。例如,Bunch[int]
是 int
的切片。我们可以以正常方式创建该类型的值:
x := Bunch[int]{1, 2, 3}
正如您所期望的,我们可以编写采用泛型类型的泛型函数:
func PrintBunch[T any](b Bunch[T]) {
方法也是:
type StringableBunch[T Stringer] []T
Golang泛型 playground
为了让您可以使用当前的泛型实现(例如尝试本教程中的代码示例),Go 团队在此处提供了启用泛型的 Go Playground 版本:
它的工作方式与我们熟知并喜爱的普通 Go playground 完全相同,只是它支持本文所述的类型参数语法。因为不可能在 playground 上运行所有的 Go 代码(例如,进行网络调用或访问文件系统的代码),所以你也可以自己从源代码中构建 Go 来尝试。
问题
Go 泛型提出来的原因是什么?
可以在此处阅读完整的设计草稿:
Type Parameters - Draft Design
Golang 会有泛型吗?
是的。如本教程中所述,目前在 Go 中支持泛型的提议已于 2020 年 6 月在一篇博客文章中宣布:The Next Step for Generics,现在已以我在此处描述的形式接受了用于添加泛型的 Github 问题。
Go 博客表示,Go 1.18 的测试版可能会包含对泛型的支持,该测试版将于 2021 年 12 月推出。
在此之前,您可以使用 Generics Playground 对其进行试验并尝试此处的示例。
Issue#43651是关于泛型的实施工作的主要跟踪问题。
标准库会采用泛型吗?
是的。至少会有一个新的 slices 包来利用新功能,本教程中描述的 constraints 包,以及其他可能的包。
您可以加入 GitHub 讨论,了解应该如何更新标准库以利用新功能。
泛型 vs 接口:有没有泛型的替代品?
正如我在 map[string] 接口教程中提到的,我们已经可以通过接口来编写处理任何类型的值的 Go 代码,而无需使用通用函数或类型。然而,如果你想写一个实现任意类型的集合等的库,使用泛型比使用接口要简单方便得多。
与 any
有什么关系?
当定义泛型函数或类型时,输入类型必须有一个约束条件。类型约束可以是接口(比如 Stringer
),类型列表(比如constraints.Ordered
),或者是关键字 comparable
。但是,如果你真的不想要任何约束,也就是说,字面上的任何类型 T
都可以呢?
表达这一点的逻辑方式是使用 interface{}
。(这个接口对一个类型的方法集完全没有说明)。但是由于这是一个非常常见的约束,预先声明的名字any
被提供作为 interface{}
的别名。你只能在类型约束中使用这个名字,所以 any
不是 interface{}
的一般同义词。
我可以使用代码生成而不是泛型吗?
在Go的泛型出现之前,"代码生成器 "方法是处理此类问题的另一种传统方式。基本上,它涉及到使用 go generate 工具为你需要在库中处理的每一个特定类型生成 Go 代码。
虽然这有效,但使用起来很尴尬,灵活性有限,并且需要额外的构建步骤。虽然代码生成对某些事情仍然有用,但我们不再需要使用它来模拟 Go 中的通用函数和类型,这一点很好。
什么是“contract”?
早期的 Go 泛型设计草案使用了与今天类似的语法,但它使用新的关键字 contract
来实现类型约束,而不是使用现有的 interface 接口。由于各种原因,这并不受欢迎,现在已经被取代了。
翻译后记:虽然我还没来得及感受泛型编程带给我的体验,但是还是很想推荐一下耗子哥的博客。
GO编程模式 : 泛型编程
评论(0)