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

2021-08-05 11:20:11 浏览数 (1)

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

目录

  • 映射、规约与过滤
  • 应用场景探索
  • 泛型

Map/Reduce/Filter

代码语言:javascript复制
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("%vn", x)
 // [HAO CHEN MEGAEASE]

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

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

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

Scenarios

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

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

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

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

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

Generic

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

代码语言:javascript复制
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服务器中。

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

0 人点赞