Go泛型系列:再简化,省略接口

这是Go泛型系列文章。

其它Go泛型文章:

如果你一直关注Go泛型的设计和实现,一定知道Go泛型代码实现是通过类型参数(type parameter)实现的,当运行泛型代码时,类型参数(type parameter)由类型参数(type argument)替代。(很遗憾parameter和argument都被翻译成了中文参数)

类型参数(type parameter)也有类型,也就是描述这个参数类型行为的元数据,被成为约束(constraint)。最通用的约束就是内建的any类型,它代表任意的类型:

1
2
3
4
5
func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

在Go泛型设计中, 约束是通过接口类型来实现的(interface)。因为接口类型和约束的功能黑类似,就是限定type argument必须实现type parameter的约束(方法集)。当然,为了实现泛型的功能,除了方法集之外,Go还对用来当做约束的接口做了扩展,定义了类型集(type set)的概念,比如下面是约束代表一个type argument可以是int、int8、int16、int32或int64的类型,是并(union)的关系,所以使用|符号。

1
2
3
type Signed interface {
int | int8 | int16 | int32 | int64
}

更进一步,Go还定义了~的符号,代表只要底层类型都是某个特定类型就可以,所以上面的例子可以写的更通用一些:

1
2
3
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

这样type MyInt int定义的MyInt类型的实例也满足这个约束。

constraints 包

Go目前的实现新增加一个package,叫做constraints,用来定义内建的约束,比如常见的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Integer interface {
Signed | Unsigned
}
type Float interface {
~float32 | ~float64
}
type Complex interface {
~complex64 | ~complex128
}
type Ordered interface {
Integer | Float | ~string
}

甚至 Russ Cox、Ian Lance Taylor他们 提议和讨论为 slice、map、chan增加必要的约束,因为它们太常用了,标准库中都可以用到。(#47203#4731947330#)。

1
2
3
4
5
6
7
8
9
10
11
type Slice[Elem any] interface {
~[]Elem
}
type Map[Key comparable, Val any] interface {
~map[Key]Val
}
type Chan[Elem any] interface {
~chan Elem
}

Rob Pike 最近新提交了一个issue,建议在Go 1.18中不要对标准库增加泛型的支持#48918。离Go 1.18发布就四个月了,很多实现还在摸索之中,这是大师给出的一个很中肯的建议,建议相关的哭的改动先增加到扩展库中(x/exp),成熟后再加到标准库中,得到了很多Gopher的赞同。这是另外一个话题了。

constraints定义常用的约束可以很好的帮助我们开发,但是你有没有感觉有点异常?

省略接口

是的,依照Go泛型规范,我们必须定义一个约束,然后才能在泛型类型和泛型方法中使用,和其它语言的泛型定义相比,你有没有觉得这一点有脱裤子放屁多此一举的味道?

你看上面的Slice、Map、Chan的定义,是不是很冗余?为什么我们不能直接在泛型类型和方法的定义中直接使用~[]Elem~map[Key]Val~chan Elem呢?

因此fzipp提议,对于一个非接口的类型,默认等价为一个约束#48424,下面的公式很好的描述了这个功能:

1
[T nonInterfaceType] ≡ [T interface{~nonInterfaceType}]

在泛型的定义中,非接口类型nonInterfaceType等价于约束interface{~nonInterfaceType}, 比如~int等价于interface{~int}。这样我们就可以省略constraints包了。 这个提议北接收了,而且相关功能也加入到了go master分支中。

mattn的Go泛型例子中,将一个整形数组转换成一个chan的例子(我稍微改动成更地道的Go的写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"constraints"
"context"
"fmt"
)
func makeChan[T constraints.Chan[E], E any](ctx context.Context, arr []E) T {
ch := make(T)
go func() {
defer close(ch)
for _, v := range arr {
select {
case <-ctx.Done():
return
case ch <- v:
}
}
}()
return ch
}
func main() {
for v := range makeChan(context.Background(), []int{1, 2, 3}) {
fmt.Println(v)
}
}

这里使用的是constraints.Chan[E]代表一个泛型的channel,现在可以用更简便的方法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"context"
)
func makeChan[T chan E, E any](ctx context.Context, arr []E) T {
ch := make(T)
go func() {
defer close(ch)
for _, v := range arr {
select {
case <-ctx.Done():
return
case ch <- v:
}
}
}()
return ch
}

直接使用chan E就可以了,方不方便?

chan E 隐式地代表interface {chan E},使用起来更简捷,不需要额外的接口(约束)定义。

虽然Go 1.18的临近,感觉Go泛型的开发工作越来越重,甚至有一些还不明确的地方,祝福一下吧,希望它顺顺利利的推出。