Go泛型边界探究,Go Stream是如何解决Go不支持泛型方法的问题的?

有很多东西我们知道能干什么怎么干,但是有些东西,知道它不能干什么才是真的了解它。

大家好,我是Coder哥,最近在用Go语言写项目,也在用泛型解决一些问题,但是也发现了一些问题,今天我们就来聊聊Go语言中泛型函数和泛型方法。

起因是这样的,作为java开发,发现Go没有类似于java8 stream一样的流处理框架,导致有些逻辑一行能实现的却要写好多行来解决,刚好Go语言也出了泛型,想着用泛型来写应该能和stream一个效果,于是就有了Go-Stream 这个项目,在写Go Stream和用的时候发现了一个关于Golang泛型的一个很有意思的问题,想着拿出来聊一下。咱还是循序渐进的展开分析:

  1. go-stream框架的简介
  2. 发现问题的过程。
  3. Go泛型为什么不支持泛型方法?
  4. go-stream框架是怎么解决这个问题的。

go-stream简介

Go-Stream实现了 java8 stream框架常用的操作,包括 过滤(Filter),转换一对一(Map), 转换一对多(FlatMap),转Map(toMap), 聚合(Reduce),数据统计(Statistic), 分组(GroupingBy)已经分组后对各组排序 等功能,基本满足99%的开发需求。

【Go泛型】用它处理切片,能省80%的代码

Go-stream代码地址:github.com/todocoder/g...

使用可参阅测试类:github.com/todocoder/g...

require github.com/todocoder/go-stream v1.1.0

发现问题

科普一下:

方法:是一个代码块,由与对象关联的名称调用。

函数:函数是按名称调用的代码,不需要与对象关联。

写完第一版,基本上能实现一堆花里胡哨的链式调用,看起来也很丝滑,比如我想对一个切片做一系列操作,最后得出结果,代码如下:

go 复制代码
func TestStream(t *testing.T) {
  items := []TestItem{
      {itemNum: 7, itemValue: "item7"},{itemNum: 6, itemValue: "item6"},
      {itemNum: 1, itemValue: "item1"},{itemNum: 2, itemValue: "item2"},
      {itemNum: 3, itemValue: "item3"},{itemNum: 4, itemValue: "item4"},
      {itemNum: 5, itemValue: "item5"},{itemNum: 5, itemValue: "item5"},
      {itemNum: 5, itemValue: "item5"},{itemNum: 8, itemValue: "item8"},
    }
    res := Of(items...).Filter(func(item TestItem) bool {
      // 过滤掉1的值
      return item.itemNum != 4
    }).Distinct(func(item TestItem) any {
      // 按itemNum 去重
      return item.itemNum
    }).Sorted(func(a, b TestItem) bool {
      // 按itemNum升序排序
      return a.itemNum < b.itemNum
    }).Skip(1).Limit(6).Reverse().ToSlice()
    fmt.Println(res)
}
  1. 使用Filter过滤掉1的值
  2. 通过Distinct对itemNum 去重(在第1步的基础上,下面同理在上一步的基础上)
  3. 通过Sorted 按itemNum升序排序
  4. 用Skip 从下标为1的元素开始
  5. 使用Limit截取排在前6位的元素
  6. 使用Reverse 对流中元素进行返转操作
  7. 使用collect终止操作将最终处理后的数据收集到Slice中

看到上面的流程作为一个多年的Javer感觉如此丝滑堪称完美,输出的结果也是原来的类型TestItem

但是我们用stream处理问题仅仅是因为一些简单的单一类型的场景么,那肯定不是了,有人说我想通过这个实现一些类型转换,或者分组,再对各个组的列表按某个字段排列,比如如下的问题:

班级有一组学号{1,2,3,....,12},对应12个人的信息在内存里面存着

go 复制代码
type Student struct {
   Num   int
   Score int
   Age   int
   Name  string
}
studentMap := map[int]Student{
      1: {Num: 1, Name: "小明", Score: 3, Age: 26},
      2: {Num: 2, Name: "小红", Score: 4, Age: 27},
      3: {Num: 3, Name: "小李", Score: 5, Age: 24},
      4: {Num: 4, Name: "老王", Score: 1, Age: 23},
      5: {Num: 5, Name: "小王", Score: 2, Age: 24},
      6: {Num: 6, Name: "小绿", Score: 2, Age: 24},
      7: {Num: 7, Name: "小蓝", Score: 3, Age: 29},
      8: {Num: 8, Name: "小橙", Score: 3, Age: 30},
      9: {Num: 9, Name: "小黄", Score: 4, Age: 29},
      10: {Num: 10, Name: "小黑", Score: 5, Age: 15},
      11: {Num: 11, Name: "小紫", Score: 3, Age: 15},
      12: {Num: 12, Name: "小刘", Score: 2, Age: 15},
}

我想把这学号转换 成具体的Student 类,然后过滤掉Score为 1的,并且再按评分 Score分组,最后对分好后的各组按照Age 降序排列,按最初v1.0.*版本的代码是这样的:

go 复制代码
// v1.0.* 的代码这样实现
res := Of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12).Map(func(n int) any {
      return studentMap[n]
   }).Filter(func(s any) bool {
      // 这里需要强转
      tempS := s.(Student)
      // 过滤掉1的
      return tempS.Score != 1
   }).Collect(collectors.GroupingBy(func(t any) int {
      return t.(Student).Score
   }, func(t any) any {
      return t
   }, func(t1 []any) {
      sort.Slice(t1, func(i, j int) bool {
         return t1[i].(Student).Age < t1[j].(Student).Age
      })
   }))
   println(res)

上面这个代码有个问题是 经过Map转换后会丢失类型 需要用 any 接收,在用的时候需要强转成目标类型,并且最后得到res 的结果是 any 类型的,用的时候也需要转换成目标类型,这样用起来非常麻烦,但是如果按这样的流式处理,这个问题不能避免。因为官方明确说明,目前Go语言不支持泛型方法

如果支持泛型方法,按找目前的编译机制,可能需要修改编译器而且会比较复杂

为什么Go泛型不好实现泛型方法?

有兴趣的可以查看官方说明:go.googlesource.com/proposal/+/...

如果支持泛型方法,考虑下面一个例子,一共有四个package:

go 复制代码
package p1
// S 是一个普通的struct,但是包含一个泛型方法Identity.
type S struct{}
// Identity 一个泛型方法,支持任意类型.
func (S) Identity[T any](v T) T { return v }
go 复制代码
package p2
// HasIdentity 定义了一个接口,支持任意实现了泛型方法Identity的类型.
type HasIdentity interface {
   Identity[T any](T) T
}
go 复制代码
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)
      }
   }
}
go 复制代码
package p4
import (
   "p1"
   "p3"
)
// CheckSIdentity 传参S给CheckIdentity.
func CheckSIdentity() {
   p3.CheckIdentity(p1.S{})
}

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

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

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

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

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

期待后面的版本加上。

问题是发现,但是要怎么解决这个问题呢,就是我想直接输出可用的类型,而不是any,因为它用起来实在是太麻烦了

go-stream框架是怎么处理这样的场景的呢

之前用过python 的 groupby 和map, python是这么做的

python 复制代码
student_group = groupby(stus, key=lambda s: s['score'])

它是把数组作为groupby的方法传过去,后面是我们的操作,那我们是不是也可以用类似这样的方式来实现呢?刚好Go语言支持泛型函数 ,就开搞,于是就有了Go-Stream v1.1.0版了,加了几个泛型转换函数,API如下:

转换函数

通过这几个函数你可以实现类型转换,分组,flatmap 等处理

注意:这几个函数 非常有用,也是最常用的,由于Go语言泛型的局限性,Go语言方法不支持自己独立的泛型,所以导致用Stream中的方法转换只能用 interface{} 代替,这样会有个非常麻烦的问题就是,转换后用的时候必须得强转才能用,所以我把这些写成转换函数,就不会受制于类(struct) 的泛型了。

API 功能说明
Map() 类型转换(优点:和上面的Map不一样的是,这里转换后可以直接使用,不需要强转)
FlatMap() 按照条件将已有元素转换为另一个对象类型,一对多逻辑,即原来一个元素对象可能会转换为1个或者多个新类型的元素,返回新的stream流(优点:同Map)
GroupingBy() 对元素进行逐个遍历,然后执行给定的处理逻辑
Collect() 将流转换为指定的类型,通过collectors.Collector进行指定(优点:转换后的类型可以直接使用,无需强转)

通过这几个函数实现上面的分组转换功能要怎么操作呢?

V1.1.0 版本的实现

go 复制代码
// v1.1.* 的代码这样实现
res := GroupingBy(Map(Of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), func(n int) Student {
      // 注意 这里的返回类型可以是目标类型了
      return studentMap[n]
   }).Filter(func(s Student) bool {
      // 这里过滤也不需要转换类型
      // 过滤掉1的
      return s.Score != 1
   }), func(t Student) int {
   // key
      return t.Score
   }, func(t Student) Student {
    // v item
      return t
   }, func(t1 []Student) {
      // 按年龄降序排列
      sort.Slice(t1, func(i, j int) bool {
         return t1[i].Age > t1[j].Age
      })
   })
   println(res)

可以看到,中间处理的时候不用转换,结果也都是强类似的

res 类型:map[int] []Student 返回值的类型我们可以直接用不用转换

虽然我们不能流式的处理不同的类型,好在用泛型函数也能解决,期待官方后续的版本支持泛型方法,stream处理列表真的非常丝滑,用过的都说好。。哈哈哈。。。

最后

作为一个Java开发,用习惯了Stream操作,在网上也没找到合适的轻量的stream框架,也不知道后续官方是否会出,在这之前,就只能先自己实现了,后面遇到复杂的处理流程会持续的更新到上面 除了除了仓库首页README里面的功能,还有并行流处理,数据的统计,支持各种分组,转换等等,有兴趣可以自行查看体验测试类:stream_test

有什么问题可以在github上提issues 留言或者公号搜:todocoder,看到后第一时间回复,感谢大家的支持!

相关推荐
千|寻9 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱23 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯29 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响32 分钟前
枚举在实际开发中的使用小Tips
后端
wuhunyu37 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi37 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
程序员清风3 小时前
阿里二面:Kafka 消费者消费消息慢(10 多分钟),会对 Kafka 有什么影响?
java·后端·面试