Go泛型不支持泛型方法,这是一个悲伤的故事

根据Go 泛型提案的描述,Go不支持泛型方法:No parameterized methods。主要原因Go泛型的处理是在编译的时候实现的,泛型方法在编译的时候,如果没有上下文的分析推断,很难判断泛型方案该如何实例化,甚至判断不了,导致目前(Go 1.18)Go实现中不支持泛型方案。

不过,泛型方法的缺失,多多少少给程序员带来一丝丝的忧伤的情绪,在一些场景之下,使用起来特别不方便。我最近看到了几个因为缺乏泛型方法导致的问题,在本文中总结一下,和大家探讨。

有一点点让人欣慰的是,Ian Lance Taylor和Ian Lance Taylor并没有把话说绝,说不定在某个版本中,泛型方法又支持了:

So while parameterized methods seem clearly useful at first glance, we would have to decide what they mean and how to implement that.

为啥当前Go泛型不好实现泛型方法?

考虑下面一个例子,一共有四个package:

1
2
3
4
5
6
7
package p1
// S 是一个普通的struct,但是包含一个泛型方法Identity.
type S struct{}
// Identity 一个泛型方法,支持任意类型.
func (S) Identity[T any](v T) T { return v }
1
2
3
4
5
6
package p2
// HasIdentity 定义了一个接口,支持任意实现了泛型方法Identity的类型.
type HasIdentity interface {
Identity[T any](T) T
}
1
2
3
4
5
6
7
8
9
10
11
12
package p3
import "p2"
// CheckIdentity 是一个普通函数,检查实参是不是实现了HasIdentity接口,如果是,则调用这个接口的泛型方法Identity.
func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity[int](0); got != 0 {
panic(got)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
package p4
import (
"p1"
"p3"
)
// CheckSIdentity 传参S给CheckIdentity.
func CheckSIdentity() {
p3.CheckIdentity(p1.S{})
}

一切看起来都没有问题,但是问题是package p3不知道p1.S类型,整个程序中如果也没有其它地方调用p1.S.Identity,依照现在的Go编译器的实现,是没有办法为p1.S.Identity[int]生成对应的代码的。

是的,如果go编译器做的比较复杂,在编译的时候这个场景是可以识别出来的,但是它需要遍历整体的程序调用链以便生成全部可能的泛型方法,对编译时间和编译器复杂性带来很大的调整。另外一点,如果代码中通过反射调用的话,编译器可能会遗漏一些泛型方法的实现,这就很要命了。

如果在运行时实现呢?就需要JIT或者反射等技术,这会造成运行时性能的下降。

很难实现啊?如果规定泛型方法不能实现接口呢?那么这类的泛型方法的存在的意义是什么呢?

所以目前没有太好的手段去实现泛型方法,暂时搁置了。

如果真的有必要,你可以通过实现泛型函数来实现泛型方法,把方法的receiver当成第一个参数传递过去。

这可以解决一部分问题,但是在使用的过程中多多少少有些麻烦。

因为泛型方法的缺乏,大家在开始使用泛型的时候就遇到了麻烦,最近连续看到多篇关于这方面的问题,比如下面几个。

Facilitator模式 by rakyll

昨天rakyll写了一篇文章https://rakyll.org/generics-facilititators/,介绍她遇到的困难以及解决方式。这也是促进我把这几天看到的case总结的原因。

如果你熟悉其它编程语言,在使用orm框架的时候,可能见过下面类似的代码,实现泛型方法进行某种对象的查询:

1
2
3
4
5
6
7
8
9
db, err := database.Connect("....")
if err != nil {
log.Fatal(err)
}
all, err := db.All[Person](ctx) // Reads all person entities
if err != nil {
log.Fatal(err)
}

因为Go缺乏泛型方法的实现,你不能实现泛型All方法,那么怎么实现呢?一种方式是实现All函数,另一种实现是实现rakyll称之为的Facilitator模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package database
type Client struct{ ... }
type Querier[T any] struct {
client *Client
}
func NewQuerier[T any](c *Client) *Querier[T] {
return &Querier[T]{
client: c,
}
}
func (q *Querier[T]) All(ctx context.Context) ([]T, error) {
// implementation
}
func (q *Querier[T]) Filter(ctx context.Context, filter ...Filter) ([]T, error) {
// implementation
}

函数实现让人感觉到一种无力感,一种缺乏归宿感,一种没有对象的感觉,而这种实现呢,生成了特定类型的Querier[T],All方法就有泛型的感觉了(虽然实际是Receiver泛型)

泛型signleflight

有些同学熟悉Go官方扩展库x/sync/singleflight,这个库很好的解决大并发的时候并发访问的问题,常常用在cache访问和微服务访问的处理之中。

为了支持任意类型,它内部的实现是使用interface{}(any)类型来表示和处理的:

1
2
3
4
5
var g Group
v, _, _ := g.Do("key", func() (interface{}, error) {
return "bar", nil
})
useString(v.(string))

五天前,有人把它改造成泛型的方式:marwan-at-work/singleflight,上面的代码使用起来改变成如下方式:

1
2
3
4
5
var g Group[string]
v, _, _ := g.Do("key", func() (string, error) {
return "bar", nil
})
useString(v)

相当于把Group改造成泛型类型,而不是实现泛型方法Do(当然目前Go泛型也实现不了)。

这个处理和上面rakyll处理方式类型,都是生成泛型类型,通过Receiver实现泛型的方法的处理。

不过对于这种方式,有一点不好的地方就是每种类型你都得生成一个特别的对象,略显麻烦。

map reduce

更早时候关于泛型的讨论,有人提出泛型方法的缺乏导致Go实现map reduce类似的库的困难,具体在哪里提到的我已经忘记了。

比如下面的实现一个iter的map reduce:

1
func (i *iter[T any]) map[K ~string](mapFn func(t T) K) *iter[K]

这种情况下用户想传入任意的K,把原先T类型的iter转换成K类型的iter,这种就不想其它支持泛型语言那么好实现了。