共计 6654 个字符,预计需要花费 17 分钟才能阅读完成。
前言
2021.12.14 日,Go 官网正式公布了反对泛型的 Go 1.18beta1 版本,这是 Go 语言自 2007 年诞生以来,最重大的性能改革。
泛型外围就 3 个概念:
-
Type parameters for functions and types
类型参数,能够用于泛型函数以及泛型类型
-
Type sets defined by interfaces
Go 1.18 之前,interface 用来定义方法集(a set of methods)。
Go 1.18 开始,还能够应用 interface 来定义类型集(a set of types),作为类型参数的 Type constraint(类型限度)
-
Type inference
类型推导,能够帮忙咱们在写代码的时候不必传递类型实参,由编译器自行推导。
留神:类型推导并不是永远都可行。
Type parameters(类型参数)
[P, Q constraint1, R constraint2]
这里定义了一个类型参数列表(type parameter list),列表里能够蕴含一个或者多个类型参数。
P,Q
和 R
都是类型参数,contraint1
和 contraint2
都是类型限度(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)
留神:
- 应用
constraints.Ordered
类型,须要import constraints
。 min[int](2, 3)
是在对泛型函数min
实例化 (instantiation),在编译期将泛型函数里的类型参数T
替换为int
。
instantiation(实例化)
泛型函数的实例化做 2 个事件
-
把泛型函数的类型参数替换为类型实参(type argument)。
比方下面的例子,min 函数调用传递的类型实参是
int
,会把泛型函数的类型参数T
替换为int
-
查看类型实参是否满足泛型函数定义里的类型限度。
对于上例,就是查看类型实参
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)
,也就是
- 先实例化失去一个非泛型函数
- 而后再做真正的函数执行。
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.Ordered
,contraints.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}
Integer
和 Float
也是定义在 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 个作用:
- 用于约定无效的类型实参,不满足类型限度的类型实参会被编译器报错。
- 如果类型限度里的所有类型都反对某个操作,那在代码里,对应的类型参数就能够应用这个操作。
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 没有传递类型实参,编译器是依据函数实参 a
和b
推导出类型实参。
类型推导能够让咱们的代码更简洁,更具可读性。
Go 泛型有 2 种类型推导:
-
function argument type inference: deduce type arguments from the types of the non-type arguments.
通过函数的实参推导进去具体的类型。比方下面例子里的
m2 = min(a, b)
,就是依据a
和b
这 2 个函数实参推导进去
T
是float64
。 -
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]) }
什么时候不要应用泛型?
-
只是单纯调用实参的办法时,不要用泛型。
// 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.Writer
的Write
办法,把内容写到指定中央。应用interface
作为参数更适合,可读性更强。 - 当函数或者办法或者具体的实现逻辑,对于不同类型不一样时,不要用泛型。比方
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…