关于golang:一文读懂Go泛型设计和使用场景

6次阅读

共计 6654 个字符,预计需要花费 17 分钟才能阅读完成。

前言

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
}

// 形式 1
m := min[int](2, 3)
// 形式 2
fmin := 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 int
    
    func (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 argument
m1 = 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 []int32
    
    func 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. 只是单纯调用实参的办法时,不要用泛型。

    // good
    func foo(w io.Writer) {b := getBytes()
       _, _ = w.Write(b)
    }
    
    // bad
    func 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…
正文完
 0