大家好,我是Coder哥,有几个月没更了,最近也在用Go语言写一些东西,作为一个Java开发,上手Go语言多少还是感觉有点不是很习气,其间一个原因是在列表处理Go中没有合适的类似于Java Stream框架的处理,这点感觉不是很方便。所以也一直在找,看到一个比较接近的方案是 go-zero中的流处理,可是局限性也比较强,没有Collectors的一些处理。那爽性就自己完成一个吧,因为最懂自己的还是自己,所以就有了这个东西。

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

Go Stream简介

  在JAVA中,涉及到对数组、Collection等集合类中的元素进行操作的时分,通常会通过循环的办法进行逐一处理,或许运用Stream的办法进行处理。那么在Go中用的多的是切片,那么这里根据Java的stream的操作习气用Go语言( 1.18+)的泛型和通道完成了一些简单的流操作功用。

go-stream git代码地址:github.com/todocoder/g…
go-stream gitee代码地址:gitee.com/todocoder/g…

Stream 介绍

  能够将Stream流操作分为3种类型:Stream的生成,Stream中心处理,Stream的停止

Stream的生成

  首要担任新建一个Stream流,或许根据现有的数组创建出新的Stream流。

API 功用阐明
Of() 通过可变参数(values ...T)创建出一个新的stream串行流目标
OfParallel() 通过可变参数(values ...T)创建出一个可并行履行stream串行流目标
OfFrom() 通过办法生成(generate func(source chan<- T))创建出一个新的stream串行流目标
OfFromParallel() 通过办法生成(generate func(source chan<- T))创建出一个可并行履行stream串行流目标
Concat() 多个流拼接的办法创建出一个串行履行stream串行流目标

Stream中心处理

  首要担任对Stream进行处理操作,并回来一个新的Stream目标,中心处理操作能够进行叠加。

API 功用阐明
Filter() 依照条件过滤符合要求的元素, 回来新的stream流
Map() 依照条件将已有元素转化为另一个目标类型,1对1逻辑,回来新类型的stream流
FlatMap() 依照条件将已有元素转化为另一个目标类型,一对多逻辑,即本来一个元素目标可能会转化为1个或许多个新类型的元素,回来新的stream流
Skip() 跳过当时流前面指定个数的元素
Limit() 仅保留当时流前面指定个数的元素,回来新的stream流
Concat() 多个流拼接到当时流下
Distinct() 依照条件去重符合要求的元素, 回来新的stream流
Sorted() 依照条件对元素进行排序, 回来新的stream流
Reverse() 对流中元素进行返转操作
Peek() 对stream流中的每个元素进行逐一遍历处理,回来处理后的stream流

Stream的停止

  通过停止函数操作之后,Stream流将会完毕,最终可能会履行某些逻辑处理,或许是依照要求回来某些履行后的成果数据。

API 功用阐明
FindFirst() 获取第一个元素
FindLast() 获取最终一个元素
ForEach() 对元素进行逐一遍历,然后履行给定的处理逻辑
Reduce() 对流中元素进行聚合处理
AnyMatch() 回来此流中是否存在元素满意所提供的条件
AllMatch() 回来此流中是否全都满意条件
NoneMatch() 回来此流中是否全都不满意条件
Count() 回来此流中元素的个数
Max() 回来stream处理后的元素最大值
Min() 回来stream处理后的元素最小值
ToSlice() 将流处理后转化为切片
Collect() 将流转化为指定的类型,通过collectors.Collector进行指定

go-stream的运用

库的引进

  因为用到了泛型,支撑的版别为golang 1.18+

  1. go.mod 中参加如下装备
require github.com/todocoder/go-stream v1.0.0
  1. 履行
go mod tidy -go=1.20
go mod download

ForEach、Peek的运用

  ForEach和Peek都能够用于对元素进行遍历然后逐一的进行处理。 但Peek归于中心办法,而ForEach归于停止办法。也就是说Peek只能在流中心处理元素,没法直接履行得到成果,其后面必须还要有其它停止操作的时分才会被履行;而ForEach作为无回来值的停止办法,则能够直接履行相关操作。

package todocoder
type TestItem struct {
	itemNum   int
	itemValue string
}
func TestForEachAndPeek(t *testing.T) {
	// ForEach
	stream.Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 3, itemValue: "item3"},
	).ForEach(func(item TestItem) {
		fmt.Println(item.itemValue)
	})
	// Peek
	stream.Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 3, itemValue: "item3"},
	).Peek(func(item *TestItem) {
		item.itemValue = item.itemValue + "peek"
	}).ForEach(func(item TestItem) {
		fmt.Println(item.itemValue)
	})
}

成果如下:

item1
item2
item3
item1peek
item2peek
item3peek

从代码及成果中得知,ForEach只是用来循环流中的元素。而Peek能够在流中心修正流中的元素。

Filter、Sorted、Distinct、Skip、Limit、Reverse

  这几个是go-stream中比较常用的中心处理办法,具体阐明在上面已标出。运用的话我们能够在流中一个或多个的组合便用。

package todocoder
func TestStream(t *testing.T) {
	// ForEach
	res := stream.Of(
		TestItem{itemNum: 7, itemValue: "item7"},
		TestItem{itemNum: 6, itemValue: "item6"},
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 3, itemValue: "item3"},
		TestItem{itemNum: 4, itemValue: "item4"},
		TestItem{itemNum: 5, itemValue: "item5"},
		TestItem{itemNum: 5, itemValue: "item5"},
		TestItem{itemNum: 5, itemValue: "item5"},
		TestItem{itemNum: 8, itemValue: "item8"},
		TestItem{itemNum: 9, itemValue: "item9"},
	).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().Collect(collectors.ToSlice[TestItem]())
	fmt.Println(res)
}
  1. 运用Filter过滤掉1的值
  2. 通过Distinct对itemNum 去重(在第1步的基础上,下面同理在上一步的基础上)
  3. 通过Sorted 按itemNum升序排序
  4. 用Skip 从下标为1的元素开始
  5. 运用Limit截取排在前6位的元素
  6. 运用Reverse 对流中元素进行返转操作
  7. 运用collect停止操作将最终处理后的数据收集到Slice中

成果:

[{8 item8} {7 item7} {6 item6} {5 item5} {3 item3} {2 item2}]

AllMatch、AnyMatch、NoneMatch、Count、FindFirst、FindLast

  这些办法,均归于这里说的简单成果停止办法。代码如下:

package todocoder
func TestSimple(t *testing.T) {
	allMatch := stream.Of(
		TestItem{itemNum: 7, itemValue: "item7"},
		TestItem{itemNum: 6, itemValue: "item6"},
		TestItem{itemNum: 8, itemValue: "item8"},
		TestItem{itemNum: 1, itemValue: "item1"},
	).AllMatch(func(item TestItem) bool {
		// 回来此流中是否全都==1
		return item.itemNum == 1
	})
	fmt.Println(allMatch)
	anyMatch := stream.Of(
		TestItem{itemNum: 7, itemValue: "item7"},
		TestItem{itemNum: 6, itemValue: "item6"},
		TestItem{itemNum: 8, itemValue: "item8"},
		TestItem{itemNum: 1, itemValue: "item1"},
	).Filter(func(item TestItem) bool {
		return item.itemNum != 1
	}).AnyMatch(func(item TestItem) bool {
		// 回来此流中是否存在 == 8的
		return item.itemNum == 8
	})
	fmt.Println(anyMatch)
	noneMatch := stream.Of(
		TestItem{itemNum: 7, itemValue: "item7"},
		TestItem{itemNum: 6, itemValue: "item6"},
		TestItem{itemNum: 8, itemValue: "item8"},
		TestItem{itemNum: 1, itemValue: "item1"},
	).Filter(func(item TestItem) bool {
		return item.itemNum != 1
	}).NoneMatch(func(item TestItem) bool {
		// 回来此流中是否悉数不等于8
		return item.itemNum == 8
	})
	fmt.Println(noneMatch)
	resFirst := stream.Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 3, itemValue: "item3"},
	).FindFirst()
	fmt.Println(resFirst.Get())
	resLast := stream.Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 3, itemValue: "item3"},
	).FindLast()
	fmt.Println(resLast.Get())
}

成果:

false
true
false
{1 item1} true
{3 item3} true

Map、FlatMap

Map与FlatMap都是用于转化已有的元素为其它元素,差异点在于:

  1. Map 依照条件将已有元素转化为另一个目标类型,1对1逻辑
  2. FlatMap 依照条件将已有元素转化为另一个目标类型,一对多逻辑

比方我要把 int 1 转为 TestItem{itemNum: 1, itemValue: “item1”}

package todocoder
func TestMap(t *testing.T) {
	res := stream.Of([]int{1, 2, 3, 4, 7}...).Map(func(item int) any {
		return TestItem{
			itemNum:   item,
			itemValue: fmt.Sprintf("item%d", item),
		}
	}).Collect(collectors.ToSlice[any]())
	fmt.Println(res)
}
[{1 item1} {2 item2} {3 item3} {4 item4} {7 item7}]

那假如我要把两个字符串[“wo shi todocoder”,”ha ha ha”] 转为 [“wo”,”shi”,”todocoder”,”ha”,”ha”,”ha”] 用Map就不行了,这就需求用到FlatMap了

package todocoder
func TestFlatMap(t *testing.T) {
	// 把两个字符串["wo shi todocoder","ha ha ha"] 转为 ["wo","shi","todocoder","ha","ha","ha"]
	res := stream.Of([]string{"wo shi todocoder", "ha ha ha"}...).FlatMap(func(s string) stream.Stream[any] {
		return stream.OfFrom(func(source chan<- any) {
			for _, str := range strings.Split(s, " ") {
				source <- str
			}
		})
	}).Collect(collectors.ToSlice[any]())
	fmt.Println(res)
}
[wo shi todocoder ha ha ha]

  这里需求弥补一句,只需通过Map或许FlatMap 处理后,类型就会统一变成 any了,而不是 泛型T,如需求强制类型处理,需求手动转化一下
这个原因是Go泛型的局限性导致的,不能在struct 办法中界说其他类型的泛型,这块看后续官方是否支撑了

能够看如下代码

package todocoder
func TestMap(t *testing.T) {
	res := stream.Of(
		TestItem{itemNum: 3, itemValue: "item3"},
	).FlatMap(func(item TestItem) stream.Stream[any] {
		return Of[any](
			TestItem{itemNum: item.itemNum * 10, itemValue: fmt.Sprintf("%s+%d", item.itemValue, item.itemNum)},
			TestItem{itemNum: item.itemNum * 20, itemValue: fmt.Sprintf("%s+%d", item.itemValue, item.itemNum)},
		)
	}).Map(func(item any) any {
		// 这里需求类型转化
		ite := item.(TestItem)
		return ToTestItem{
			itemNum:   ite.itemNum,
			itemValue: ite.itemValue,
		}
	}).Collect(collectors.ToSlice[any]())
	fmt.Println(res)
}

collectors.ToMap、collectors.GroupBy

  这两个是相对杂乱的停止办法,ToMap 是类似于Java stream流中Collectors.toMap()能够把切片数组转化成 切片map, GroupBy 类似于Java stream中 Collectors.groupingby()办法,按某个维度来分组

我有如下切片列表:

TestItem{itemNum: 1, itemValue: “item1”},
TestItem{itemNum: 2, itemValue: “item2”},
TestItem{itemNum: 2, itemValue: “item3”}

  1. 第一个需求是:把这个列表按 itemNum为Key, itemValue 为 value转化成Map
  2. 第二个需求是:把这个列表按 itemNum为Key, 分组后转化成Map
    我们看一下代码:
package todocoder
func TestToMap(t *testing.T) {
	// 第一个需求
	resMap := Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 2, itemValue: "item3"},
	).Collect(collectors.ToMap[TestItem](func(t TestItem) any {
		return t.itemNum
	}, func(item TestItem) any {
		return item.itemValue
	}, func(oldV, newV any) any {
		return oldV
	}))
	fmt.Println("第一个需求:")
	fmt.Println(resMap)
	// 第二个需求
	resGroup := Of(
		TestItem{itemNum: 1, itemValue: "item1"},
		TestItem{itemNum: 2, itemValue: "item2"},
		TestItem{itemNum: 2, itemValue: "item3"},
	).Collect(collectors.GroupingBy(func(t TestItem) any {
		return t.itemNum
	}, func(t TestItem) any {
		return t
	}))
	fmt.Println("第二个需求:")
	fmt.Println(resGroup)
}
第一个需求:
map[1:item1 2:item2]
第二个需求:
map[1:[{1 item1}] 2:[{2 item2} {2 item3}]]

最终

  作为一个Java开发,用习气了Stream操作,也没找到合适的轻量的stream框架,也不知道后续官方是否会出,在这之前,就先自己简单完成一个,后面遇到杂乱的处理流程会持续的更新到上面 除了上面这些功用,还有并行流处理,有爱好能够自行查看体会测试类:stream_test

有什么问题能够留言,看到后第一时间回复