0

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

 2 years ago
source link: https://colobu.com/2021/12/22/no-parameterized-methods/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

package p1
// S 是一个普通的struct,但是包含一个泛型方法Identity.
type S struct{}
// Identity 一个泛型方法,支持任意类型.
func (S) Identity[T any](v T) T { return v }
package p2
// HasIdentity 定义了一个接口,支持任意实现了泛型方法Identity的类型.
type HasIdentity interface {
Identity[T any](T) T
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)
package p4
import (
// 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总结的原因。

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

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模式:

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)类型来表示和处理的:

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

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

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:

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK