从处理集合元素聊起 日常开发过程中,要处理数组、切片、字典等集合类型,常规做法都是循环迭代进行处理。比如将一个字典类型用户切片中的所有年龄属性值提取出来,然后求和,常规实现是通过循环遍历所有切片,然后从用户字典键值对中提取出年龄字段值,再依次进行累加,最后返回计算结果: packagemain import( fmt strconv ) funcageSum(users〔〕map〔string〕string)int{ varsumint for,user:rangeusers{ num,:strconv。Atoi(user〔age〕) sumnum } returnsum } funcmain(){ varusers〔〕map〔string〕string{ { name:张三, age:18, }, { name:李四, age:22, }, { name:王五, age:20, }, } fmt。Printf(用户年龄累加结果:d,ageSum(users)) } 执行上述代码,打印结果如下: 注:为了简化流程,这里忽略了程序出错的处理。 针对简单的单个场景,这么实现没什么问题,但这是典型的面向过程思维,而且代码几乎没有什么复用性可言:每次处理类似的问题都要编写同样的代码模板,比如计算其他字段值,或者修改类型转化逻辑,都要重新编写实现代码。 引入MapReduce 在函数式编程中,我们可以通过MapReduce技术让这个功能实现变得更优雅,代码复用性更好。 MapReduce并不是一个整体,而是要分两步实现:Map和Reduce,这个示例也正好符合MapReduce模型:先将字典类型切片转化为一个字符串类型切片(Map,字面意思就是一一映射),再将转化后的切片元素转化为整型后累加起来(Reduce,字面意思就是将多个集合元素通过迭代处理减少为一个)。 为此,我们先要实现Map映射转化函数: funcmapToString(items〔〕map〔string〕string,ffunc(map〔string〕string)string)〔〕string{ newSlice:make(〔〕string,len(items)) for,item:rangeitems{ newSliceappend(newSlice,f(item)) } returnnewSlice } 再编写Reduce求和函数: f funcfieldSum(items〔〕string,ffunc(string)int)int{ varsumint for,item:rangeitems{ sumf(item) } returnsum } 通过MapReduce重构后没有什么硬编码,类型转化和字段获取逻辑都封装到两个函数支持的函数类型参数中实现了, 在main函数中编写新的调用代码如下: ageSlice:mapToString(users,func(usermap〔string〕string)string{ returnuser〔age〕 }) sum:fieldSum(ageSlice,func(agestring)int{ intAge,:strconv。Atoi(age) returnintAge }) fmt。Printf(用户年龄累加结果:d,sum) 计算结果和之前一样,看起来代码实现比之前的简单迭代更复杂了,但是代码复用性、可读性和后续可维护性更好,毕竟,对于长期维护的项目而言,业务代码不可能一次编写好就完事了。目前来看,只要是符合上述约定参数类型的切片数据,现在都可以通过这段代码来实现指定字段值的累加功能,并且支持自定义字段和数值类型转化逻辑。 当然了,Go语言现在还不支持泛型,否则我们可以编写出抽象性更好的MapReduce代码,后面介绍完接口和反射部分后,我们再尝试在运行时通过泛型来重构这段代码的实现。 采用MapReduce技术编写类似的集合处理代码为我们引入了新的编程模式,将编程思维升级到描述一件事情要怎么干的高度,就像面向对象编程中引入设计模式那样,从而摆脱面向过程编程那种代码只是用来描述干什么,像记流水账一样的编程窠臼。 下面这张图非常形象地描述了MapReduce技术在函数式编程中扮演的角色和起到的作用: 引入Filter函数 有的时候,为了让MapReduce代码更加健壮(排除无效的字段值),或者只对指定范围的数据进行统计计算,还可以在MapReduce基础上引入Filter(过滤器),对集合元素进行过滤。 我们在上面的代码中新增一个Filter函数: funcitemsFilter(items〔〕map〔string〕string,ffunc(map〔string〕string)bool)〔〕map〔string〕string{ newSlice:make(〔〕map〔string〕string,len(items)) for,item:rangeitems{ iff(item){ newSliceappend(newSlice,item) } } returnnewSlice } 接下来,我们可以在main函数中应用Filter函数对无效用户年龄进行过滤,或者排除指定范围年龄: funcmain(){ varusers〔〕map〔string〕string{ { name:张三, age:18, }, { name:李四, age:22, }, { name:王五, age:20, }, { name:赵六, age:10, }, { name:孙七, age:60, }, { name:周八, age:10, }, } fmt。Printf(用户年龄累加结果:d,ageSum(users)) validUsers:itemsFilter(users,func(usermap〔string〕string)bool{ age,ok:user〔age〕 if!ok{ returnfalse } intAge,err:strconv。Atoi(age) iferr!nil{ returnfalse } ifintAge18intAge35{ returnfalse } returntrue }) ageSlice:mapToString(validUsers,func(usermap〔string〕string)string{ returnuser〔age〕 }) sum:fieldSum(ageSlice,func(agestring)int{ intAge,:strconv。Atoi(age) returnintAge }) fmt。Printf(用户年龄累加结果:d,sum) } 上述代码的计算结果依然是60,说明过滤器生效了。 不过分开调用Map、Reduce、Filter函数不太优雅,我们可以通过装饰器模式将它们层层嵌套起来,或者通过管道模式(Pipeline)让这个调用逻辑可读性更好,更优雅。