go stream实现

概述

在函数式编程时,经常会用到filter/map/reduce函数,对于java语言,可以通过Stream轻松进行filter/map/reduce,同时在上一篇文章中,我也介绍了Stream另外一种更为简单的实现方式,既然学习go语言,我也希望可以像java一样比较简单的通过流的方式来进行filter/map/reduce

filter/map/reduce的基本实现方式

大家看到网上的文章,通常的实现方式如下

package main

import (
	"fmt"
	"strconv"
)

func main() {

	arr := []string{"1", "2", "3", "4"}
	r := Map(arr, func(it string) int {
		i, _ := strconv.Atoi(it)
		return i
	})
	fmt.Println(r)
	r = Filter(r, func(it int) bool {
		return it%2 == 0
	})
	fmt.Println(r)
	c := Reduce(r, 0, func(init *int, it int) {
		*init += it
	}, func(i *int) int {
		return *i * 3
	})
	fmt.Println(c)

}

func Map[T, R any](arr []T, f func(T) R) []R {
	r := make([]R, len(arr))
	for i, v := range arr {
		r[i] = f(v)
	}
	return r
}

func Filter[T any](arr []T, test func(T) bool) []T {
	r := make([]T, 0)
	for _, v := range arr {
		if test(v) {
			r = append(r, v)
		}
	}
	return r
}

func Reduce[T, I, R any](arr []T, init I, action func(*I, T), end func(*I) R) R {
	for _, v := range arr {
		action(&init, v)
	}
	return end(&init)
}

通过通用的filter/map/reduce实现,我写了一个将string slice转成int slice,让后过滤出偶数,最后进行汇总后再乘以3,实际的开发过程中,map和filter可能会发生多次
但是这样的实现存在一个问题,可以看到,每次map和filter之后,都会将slice中所有数据迭代后转出新的slice,这里会存两个问题

  • 如果进行多次的map和filter,就会产生多个slice,性能有影响的同时对空间也有影响
  • 假设有一个上亿条csv文件,我们像通过map进行每行数据转换,filter过滤掉部分不要的行之后写入新的csv文件,同时在文件尾部还要写入汇总信息,基于目前的filter/map/reduce行数,显然是无法实现的

java的Stream是一种流式计算的形式,slice中的数据会一条一条流过map,filter,最后被reduce,中间不需要临时的slice来存储数据

基于go的Stream实现

参考java版本的实现方案,依然先定义一个each函数

type Stream struct {
	each func(func(*any))
}
func StreamOf(arr []any) *Stream {

	stream := Stream{each: func(consumer func(*any)) {
		for _, v := range arr {
			consumer(&v)
		}
	}}

	return &stream
}

接下来定义Map方法,方法实现上就是对原来的each函数进行套娃,套上转换逻辑

func (sm *Stream) Map(mapFunc func(*any) any) *Stream {
	oldEach := sm.each
	sm.each = func(f func(*any)) {
		oldEach(func(a *any) {
			aa := mapFunc(a)
			f(&aa)
		})
	}
	return sm
}

理解了Map方法,filter方法就比较好实现了

func (sm *Stream) Filter(testFunc func(*any) bool) *Stream {
	oldEach := sm.each
	sm.each = func(f func(*any)) {
		oldEach(func(a *any) {
			if testFunc(a) {
				f(a)
			}
		})
	}
	return sm
}

接下来实现reduce方法,先实现个toSlice方法

func (sm *Stream) ToSlice() []any {
    arr := make([]any, 0)
    sm.each(func(a *any) {
        arr = append(arr, *a)
    })
    return arr
}

再来个通用的reduce方法

func (sm *Stream) Reduce(init any, action func(*any, *any), end func(*any) any) any {
    sm.each(func(it *any) {
        action(&init, it)
    })
    return end(&init)
}

使用示例

package main

import "fmt"

func main() {
	var arr []any = []any{1, 2, 3, 4}

	r := StreamOf(arr).Map(func(it *any) any {
		return (*it).(int) + 6
	}).Map(func(it *any) any {
		return (*it).(int) / 2
	}).Filter(func(it *any) bool {
		return (*it).(int)%2 != 0
	}).toSlice()
	fmt.Println(r)

}
//输出结果为:
[3 5]

进阶,实现FindFirst函数

findFirst就找到匹配的第一个数据,找到了就中断执行,会了FindFirst的实现,anyMatch,allMatch就比较好实现了。

增加stop标记

type Stream struct {
	each func(func(*any))
	//中断标记
	stop *bool
}

func StreamOf(arr []any) *Stream {
	s := false
	stream := Stream{func(consumer func(*any)) {
		for _, v := range arr {
			//一旦中断,就跳出执行
			if s {
				break
			}
			consumer(&v)
		}
	}, &s}

	return &stream
}

FindFirst实现

func (sm *Stream) FindFirst(testFunc func(*any) bool) *any {
	var r *any = nil
	oldEach := sm.each
	sm.each = func(f func(*any)) {
		oldEach(func(a *any) {
			if testFunc(a) {
				//找到了,中断循环
				*(sm.stop) = true
				r = a
			}
		})
	}
	sm.each(func(a *any) {})
	return r
}

使用示例

package main

import "fmt"

func main() {
	var arr []any = []any{1, 2, 3, 4}

	r := StreamOf(arr).Map(func(it *any) any {
		return (*it).(int) + 6
	}).Map(func(it *any) any {
		return (*it).(int) / 2
	}).FindFirst(func(a *any) bool {
		return *a == 4
	})
	fmt.Println(*r)

}
//输出:4

总结

go的实现思路和java的实现思路实际是一致的,差异点主要是语法上的不同。
作为go的初学者,从这个功能的实现上,我发现go的泛型支持没java那么方便,go的Stream实现我本来想支持泛型的,但是折腾了半天也没搞定就放弃了,如果大家有支持泛型的Stream方式,请告诉我一下

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值