Junedayday Blog

六月天天的个人博客

0%

Go编程模式 - 6-映射、归约与过滤

注:本文的灵感来源于GOPHER 2020年大会陈皓的分享,原PPT的链接可能并不方便获取,所以我下载了一份PDF到git仓,方便大家阅读。我将结合自己的实际项目经历,与大家一起细品这份文档。

目录

Map/Reduce/Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func MapUpCase(arr []string, fn func(s string) string) []string {
var newArray = []string{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}

func MapLen(arr []string, fn func(s string) int) []int {
var newArray = []int{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}

func Reduce(arr []string, fn func(s string) int) int {
sum := 0
for _, it := range arr {
sum += fn(it)
}
return sum
}

func Filter(arr []string, fn func(n string) bool) []string {
var newArray = []string{}
for _, it := range arr {
if fn(it) {
newArray = append(newArray, it)
}
}
return newArray
}

func main() {
var list = []string{"Hao", "Chen", "MegaEase"}

// 元素一对一映射 string->string
x := MapUpCase(list, func(s string) string {
return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
// [HAO CHEN MEGAEASE]

// 元素一对一映射 string->int
y := MapLen(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", y)
// [3 4 8]

// 归约:多个元素->一个元素
z := Reduce(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", z)
// 15

// 过滤:过滤不满足条件的元素
f := Filter(list, func(s string) bool {
return len(s) > 3
})
fmt.Printf("%v\n", f)
// [Chen MegaEase]
}

Scenarios

  • Map 是一对一的场景,是 循环中对数据加工处理
  • Reduce 是多对一,是 数据聚合处理
  • Filter是过滤的处理,是 数据有效性

我们以常见的账单统计相关的功能,我们会遇上大量的此类情况:

  1. 统计消费总额 - Reduce
  2. 统计用户A - Filter
  3. 统计本月 - Filter
  4. 费用转化为美金 - Map

在综合各个因素后,就是大量复杂的、管道式的Map/Reduce/Filter操作。

延伸思考一下,这块和SQL语句非常类似

Generic

耗子叔在接下来的部分,展示了用reflect处理泛型情况。我这边简单地截取Map部分解析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
func TransformInPlace(slice, function interface{}) interface{} {
return transform(slice, function, true)
}

// map的转换函数,slice为切片,function为对应的函数,inPlace表示是否原地处理
func transform(slice, function interface{}, inPlace bool) interface{} {
// 类型判断,必须为切片
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic("transform: not slice")
}

// 函数的签名判断,即函数的入参必须和slice里的元素一致
fn := reflect.ValueOf(function)
elemType := sliceInType.Type().Elem()
if !verifyFuncSignature(fn, elemType, nil) {
panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")
}

// 如果是原地,则直接处理函数,结果会保存到入参中(这时入参一般为指针)
// 如果非原地,那就需要新建一个切片,用来保存结果
sliceOutType := sliceInType
if !inPlace {
sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
}
for i := 0; i < sliceInType.Len(); i++ {
sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
}
return sliceOutType.Interface()

}

func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {
// 类型判断
if fn.Kind() != reflect.Func {
return false
}

// 入参数量和函数签名一致,出参必须只有一个
if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
return false
}

// 每个函数入参的类型校验
for i := 0; i < len(types)-1; i++ {
if fn.Type().In(i) != types[i] {
return false
}
}

// 出参类型的校验
outType := types[len(types)-1]
if outType != nil && fn.Type().Out(0) != outType {
return false
}
return true
}

仔细阅读这一块代码,我们能学到很多反射方面的知识,尤其是并不常用的函数相关的。

但是,我不建议大家在实际项目中直接使用这一块代码,毕竟其中大量的反射操作是比较耗时的,尤其是在延迟非常敏感的web服务器中。

如果我们多花点时间、直接编写指定类型的代码,那么就能在编译期发现错误,运行时也可以跳过反射的耗时。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili:https://space.bilibili.com/293775192

公众号:golangcoding