## 介绍 这篇博文基于我们在 GopherCon 2021 上的演讲:

Go 1.18 版本增加了对泛型的支持。泛型是自第一个开源版本以来我们对 Go 所做的最大改变。在本文中,我们将介绍新的语言功能。我们不会试图涵盖所有细节,但我们会触及所有要点。有关更详细和更长的描述,包括许多示例,请参阅提案文档。有关语言更改的更准确描述,请参阅 更新的语言规范。(请注意,实际的 1.18 实施对提案文件允许的内容施加了一些限制;规范应该是准确的。未来的版本可能会取消一些限制。)

泛型是一种编写代码的方式,它独立于所使用的特定类型。现在可以编写函数和类型以使用一组类型中的任何一种。

泛型为语言添加了三个新的重要内容:

函数和类型的类型参数。

将接口类型定义为类型集,包括没有方法的类型。

类型推断,允许在调用函数时在许多情况下省略类型参数。

类型参数

现在允许函数和类型具有类型参数。类型参数列表看起来像一个普通的参数列表(除了它使用方括号而不是圆括号)。

Min 函数 为了展示泛型是如何工作的,让我们从浮点值的基本非泛型函数开始:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

我们可以通过添加类型参数列表来使这个函数通用,其实就是使其适用于不同的类型。在这个示例中,我们添加了一个带有单个类型参数的类型参数列表T,然后用T来替换float64。


func GMin[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}

现在可以通过编写类似的调用来使用类型参数调用此函数

x := GMin[int](2, 3) GMin在这种情况下,向 提供类型参数int称为 实例化。实例化分两步进行。首先,编译器在泛型函数或类型中用所有类型参数替换它们各自的类型参数。其次,编译器验证每个类型参数是否满足各自的约束。我们很快就会明白这意味着什么,但是如果第二步失败,实例化就会失败并且程序无效。

成功实例化后,我们有一个可以像任何其他函数一样调用的非泛型函数。例如,在类似的代码中

fmin := GMin[float64] m := fmin(2.71, 3.14) 实例化GMin[float64]产生了我们原来的浮点Min函数,我们可以在函数调用中使用它。

类型参数也可以与类型一起使用。

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]

这里泛型类型Tree存储类型参数的值T。泛型类型可以有方法,就像Lookup在这个例子中一样。为了使用泛型类型,它必须被实例化; Tree[string]是一个使用类型参数string实例化Tree的的示例。

类型集 让我们更深入地了解可用于实例化类型参数的类型参数。

普通函数对每个值参数都有一个类型;该类型定义了一组值。例如,上面提到的非泛型函数Min,它的参数是float64类型的,所以允许的参数值集是可以由 float64 类型表示的浮点值集。

同样,类型参数列表对每个类型参数都有一个类型。因为类型参数本身就是一种类型,所以类型参数的类型定义了类型集。这种元类型称为 类型约束。

在泛型函数 GMin 中,类型约束是从 constraints 包中导入的。Ordered约束描述了具有可以排序的值的所有类型的集合,或者换句话说,可以通过 < 运算符(或 <= 、 > 等)进行比较。该约束确保只有具有可排序值的类型才能传递给GMin. 这也意味着在GMin函数体中该类型参数的值可以用于与 < 运算符进行比较。

在 Go 中,类型约束必须是接口。即接口类型可以作为值类型,也可以作为元类型。接口定义了方法,因此我们可以表达需要某些方法存在的类型约束。但是constraints.Ordered也是接口类型,< 操作符却不是方法。

为了完成这项工作,我们以一种新的方式看待接口。

直到最近,Go 规范才说接口定义了一个方法集,大致就是接口中枚举的方法集。实现所有这些方法的任何类型都实现了该接口。