前言

2021.12.14日,Go官网正式公布了反对泛型的Go 1.18beta1版本,这是Go语言自2007年诞生以来,最重大的性能改革。

泛型外围就3个概念:

  1. Type parameters for functions and types

    类型参数,能够用于泛型函数以及泛型类型

  2. Type sets defined by interfaces

    Go 1.18之前,interface用来定义方法集( a set of methods)。

    Go 1.18开始,还能够应用interface来定义类型集(a set of types),作为类型参数的Type constraint(类型限度)

  3. Type inference

    类型推导,能够帮忙咱们在写代码的时候不必传递类型实参,由编译器自行推导。

    留神:类型推导并不是永远都可行。

Type parameters(类型参数)

[P, Q constraint1, R constraint2]

这里定义了一个类型参数列表(type parameter list),列表里能够蕴含一个或者多个类型参数。

P,QR都是类型参数,contraint1contraint2都是类型限度(type constraint)。

  • 类型参数列表应用方括号[]
  • 类型参数倡议首字母大写,用来示意它们是类型

先看一个简略示例:

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

这个例子,只能计算2个float64中的较小者。有泛型之前,如果咱们要反对计算2个int或者其它数值类型的较小者,就须要实现新的函数、或者应用interface{},或者应用Refelect

对于这个场景,应用泛型代码更简洁,效率也更优。反对比拟不同数值类型的泛型min函数实现如下:

func min(T constraints.Ordered) (x, y T) T {    if x < y {        return x    }    return y}// 调用泛型函数m := min[int](2, 3)

留神:

  1. 应用constraints.Ordered类型,须要import constraints
  2. min[int](2, 3)是在对泛型函数min实例化(instantiation),在编译期将泛型函数里的类型参数T替换为int

instantiation(实例化)

泛型函数的实例化做2个事件

  1. 把泛型函数的类型参数替换为类型实参(type argument)。

    比方下面的例子,min函数调用传递的类型实参是int,会把泛型函数的类型参数T替换为int

  2. 查看类型实参是否满足泛型函数定义里的类型限度。

    对于上例,就是查看类型实参int是否满足类型限度constraints.Ordered

任何一步失败了,那泛型函数的实例化就失败了,也就是泛型函数调用就失败了。

泛型函数实例化后就生成了一个非泛型函数,用于真正的函数执行。

下面的min[int](2, 3)调用还能够替换为如下代码:

func min(T constraints.Ordered) (x, y T) T {    if x < y {        return x    }    return y}// 形式1m := min[int](2, 3)// 形式2fmin := min[int]m2 := fmin(2, 3)

min[int](2, 3)会被编译器解析成(min[int])(2, 3),也就是

  1. 先实例化失去一个非泛型函数
  2. 而后再做真正的函数执行。

generic types(泛型类型)

类型参数除了用于泛型函数之外,还能够用于Go的类型定义,来实现泛型类型(generic types)。

看如下代码示例,实现了一个泛型二叉树构造

type Tree[T interface{}] struct {    left, right *Tree[T]    data T}func (t *Tree[T]) Lookup(x T) *Tree[T] var stringTree Tree[string]

二叉树节点存储的数据类型可能是多样的,有的二叉树存储int,有的存储string等等。

应用泛型,能够让Tree这个构造体类型反对二叉树节点存储不同的数据类型。

对于泛型类型的办法,须要在办法接收者申明对应的类型参数。比方上例里的Lookup办法,在指针接收者*Tree[T]里申明了类型参数T

type sets(类型集)

类型参数的类型限度约定了该类型参数容许的具体类型。

类型限度往往蕴含了多个具体类型,这些具体类型就形成了类型集。

func min(T constraints.Ordered) (x, y T) T {    if x < y {        return x    }    return y}

比方下面的例子,类型参数T的类型限度是constraints.Orderedcontraints.Ordered蕴含了十分多的具体类型,定义如下:

// Ordered is a constraint that permits any ordered type: any type// that supports the operators < <= >= >.// If future releases of Go add new ordered types,// this constraint will be modified to include them.type Ordered interface {  Integer | Float | ~string}

IntegerFloat也是定义在constraints这个包里的类型限度,

类型参数列表不能用于办法,只能用于函数

type Foo struct {}func (Foo) bar[T any](t T) {}

下面的例子在构造体类型Foo的办法bar应用了类型参数列表,编译会报错:

./example1.go:30:15: methods cannot have type parameters./example1.go:30:16: invalid AST: method must have no type parameters

集体认为Go的这个编译提醒:methods cannot have type paramters 不是特地精确。

比方上面的例子,就是在办法bar里用到了类型参数T,改成methods cannot have type paramter list感觉会更好。

type Foo[T any] struct {}func (Foo[T]) bar(t T) {}

留神:类型限度必须是interface类型。比方上例的constraints.Ordered就是一个interface类型。

| 和 ~

|: 示意取并集。比方下例的Number这个interface能够作为类型限度,用于限定类型参数必须是int,int32和int64这3种类型。

type Number interface{    int | int32 | int64}

~T: ~ 是Go 1.18新增的符号,~T示意底层类型是T的所有类型。~的英文读作tilde。

  • 例1:比方下例的AnyString这个interface能够作为类型限度,用于限定类型参数的底层类型必须是string。string自身以及上面的MyString都满足AnyString这个类型限度。

    type AnyString interface{   ~string}type MyString string
  • 例2:再比方,咱们定义一个新的类型限度叫customConstraint,用于限定底层类型为int并且实现了String() string办法的所有类型。上面的customInt就满足这个type constraint。

    type customConstraint interface {   ~int   String() string}type customInt intfunc (i customInt) String() string {   return strconv.Itoa(int(i))}

类型限度有2个作用:

  1. 用于约定无效的类型实参,不满足类型限度的类型实参会被编译器报错。
  2. 如果类型限度里的所有类型都反对某个操作,那在代码里,对应的类型参数就能够应用这个操作。

constraint literals(类型限度字面值)

type constraint既能够提前定义好,也能够在type parameter list里间接定义,后者就叫constraint literals。

[S interface{~[]E}, E interface{}][S ~[]E, E interface{}][S ~[]E, E any]

几个留神点:

  • 能够间接在方括号[]里,间接定义类型限度,即应用类型限度字面值,比方上例。
  • 在类型限度的地位,interface{E}也能够间接写为E,因而就能够了解interface{~[]E}能够写为~[]E
  • any是Go 1.18新增的预申明标识符,是interface{}的别名。

constraints包

constraints包定义了一些罕用的类型限度,整个包除了测试代码,就1个constraints.go文件,50行代码,源码地址:

https://github.com/golang/go/...

蕴含的类型限度如下:

  • constraints.Signed

    type Signed interface {    ~int | ~int8 | ~int16 | ~int32 | ~int64}
  • constraints.Unsigned

    type Unsigned interface {    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr}
  • constraints.Integer

    type Integer interface {    Signed | Unsigned}
  • constraints.Float

    type Float interface {    ~float32 | ~float64}
  • constraints.Complex

    type Complex interface {    ~complex64 | ~complex128}
  • constraints.Ordered

    type Ordered interface {    Integer | Float | ~string}

Type inference(类型推导)

咱们看上面的代码示例:

func min(T constraints.Ordered) (x, y T) T {    if x < y {        return x    }    return y}var a, b, m1, m2 float64// 形式1:显示指定type argumentm1 = min[float64](a, b)// 形式2:不指定type argument,让编译器自行推导m2 = min(a, b)

形式2没有传递类型实参,编译器是依据函数实参ab推导出类型实参。

类型推导能够让咱们的代码更简洁,更具可读性。

Go泛型有2种类型推导:

  1. function argument type inference: deduce type arguments from the types of the non-type arguments.

    通过函数的实参推导进去具体的类型。比方下面例子里的m2 = min(a, b),就是依据ab这2个函数实参

    推导进去Tfloat64

  2. constraint type inference: inferring a type argument from another type argument, based on type parameter constraints.

    通过曾经确定的类型实参,推导出未知的类型实参。上面的代码示例里,依据函数实参2不能确定E是什么类型,然而能够确定S[]int32,再联合类型限度里S的底层类型是[]E,能够推导出E是int32,int32满足constraints.Integer限度,因而推导胜利。

    type Point []int32func ScaleAndPrint(p Point) {  r := Scale(p, 2)  fmt.Println(r)}func Scale[S ~[]E, E constraints.Integer](s S, c E) S {  r := make(S, len(s))  for i, v := range s {    r[i] = v * c  }  return r}

类型推导并不是肯定胜利,比方类型参数用在函数的返回值或者函数体内,这种状况就必须指定类型实参了。

func test[T any] () (result T) {...}func test[T any] () {  fmt.Println(T)}

更深刻理解type inference能够参考:https://go.googlesource.com/p...

应用场景

箴言

Write code, don't design types.

在写Go代码的时候,对于泛型,Go泛型设计者Ian Lance Taylor倡议不要一上来就定义type parameter和type constraint,如果你一上来就这么做,那就搞错了泛型的最佳实际。

先写具体的代码逻辑,等意识到须要应用type parameter或者定义新的type constraint的时候,再加上type parameter和type constraint。

什么时候应用泛型?

  • 须要应用slice, map, channel类型,然而slice, map, channel里的元素类型可能有多种。
  • 通用的数据结构,比方链表,二叉树等。上面的代码实现了一个反对任意数据类型的二叉树。

    type Tree[T any] struct {  cmp func(T, T) int  root *node[T]}type node[T any] struct {  left, right *node[T]  data T}func (bt *Tree[T]) find(val T) **node[T] {  pl := &bt.root  for *pl != nil {    switch cmp := bt.cmp(val, (*pl).data); {      case cmp < 0 : pl = &(*pl).left      case cmp > 0 : pl = &(*pl).right    default: return pl    }  }  return pl}
  • 当一个办法的实现对所有类型都一样。

    type SliceFn[T any] struct {  s []T  cmp func(T, T) bool}func (s SliceFn[T]) Len() int{return len(s.s)}func (s SliceFn[T]) Swap(i, j int) {  s.s[i], s.s[j] = s.s[j], s.s[i]}func (s SliceFn[T]) Less(i, j int) bool {  return s.cmp(s.s[i], s.s[j])}

什么时候不要应用泛型?

  1. 只是单纯调用实参的办法时,不要用泛型。

    // goodfunc foo(w io.Writer) {   b := getBytes()   _, _ = w.Write(b)}// badfunc foo[T io.Writer](w T) {   b := getBytes()   _, _ = w.Write(b)}

    比方下面的例子,单纯是调用io.WriterWrite办法,把内容写到指定中央。应用interface作为参数更适合,可读性更强。

  2. 当函数或者办法或者具体的实现逻辑,对于不同类型不一样时,不要用泛型。比方encoding/json这个包应用了reflect,如果用泛型反而不适合。

总结

Avoid boilerplate.

Corollary: Don't use type parameters prematurely; wait until you are about to write boilerplate code.

不要轻易应用泛型,Ian给的倡议是:当你发现针对不同类型,会写出同样的代码逻辑时,才去应用泛型。也就是

Avoid boilerplate code

Go语言里interface和refelect能够在某种程度上实现泛型,咱们在解决多种类型的时候,要思考具体的应用场景,切勿自觉用泛型。

想更加深刻理解Go泛型设计原理的能够参考Go泛型设计作者Ian和Robert写的Go Proposal:

https://go.googlesource.com/p...

开源地址

文章和代码开源地址在GitHub: https://github.com/jincheng9/...

公众号:coding进阶

集体网站:https://jincheng9.github.io/

References

  • 官网教程:Go泛型入门
  • GopherCon 2021 Talk on Generics
  • Go Generics Proposal
  • https://teivah.medium.com/whe...
  • https://bitfieldconsulting.co...