从处理集合元素聊起
日常开发过程中,要处理数组、切片、字典等集合类型,常规做法都是循环迭代进行处理。比如将一个字典类型用户切片中的所有年龄属性值提取出来,然后求和,常规实现是通过循环遍历所有切片,然后从用户字典键值对中提取出年龄字段值,再依次进行累加,最后返回计算结果:
package main
import (
"fmt"
"strconv"
)
func ageSum(users []map[string]string) int {
var sum int
for _, user := range users {
num, _ := strconv.Atoi(user["age"])
sum += num
}
return sum
}
func main() {
var users = []map[string]string{
{
"name": "张三",
"age": "18",
},
{
"name": "李四",
"age": "22",
},
{
"name": "王五",
"age": "20",
},
}
fmt.Printf("用户年龄累加结果: %d\n", ageSum(users))
}
执行上述代码,打印结果如下:
用户年龄累加结果: 60
针对简单的单个场景,这么实现没什么问题,但这是典型的面向过程思维,而且代码几乎没有什么复用性可言:每次处理类似的问题都要编写同样的代码模板,比如计算其他字段值,或者修改类型转化逻辑,都要重新编写实现代码.
引入 Map-Reduce
在函数式编程中,我们可以通过 Map-Reduce 技术让这个功能实现变得更优雅,代码复用性更好。
Map-Reduce 并不是一个整体,而是要分两步实现:Map 和 Reduce,这个示例也正好符合
Map-Reduce 模型:先将字典类型切片转化为一个字符串类型切片(Map,字面意思就是一一映射),再将转化后的切片元素转化为整型后累加起来(Reduce,字面意思就是将多个集合元素通过迭代处理减少为一个)。
为此,我们先要实现 Map 映射转化函数:
func mapToString(items []map[string]string, f func(map[string]string) string) []string {
newSlice := make([]string, len(items))
for _, item := range items {
newSlice = append(newSlice, f(item))
}
return newSlice
}
再编写 Reduce 求和函数:
func fieldSum(items []string, f func(string) int) int {
var sum int
for _, item := range items{
sum += f(item)
}
return sum
}
通过 Map-Reduce 重构后没有什么硬编码,类型转化和字段获取逻辑都封装到两个函数支持的函数类型参数中实现了,在 main 函数中编写新的调用代码如下:
ageSlice := mapToString(users, func(user map[string]string) string {
return user["age"]
})
sum := fieldSum(ageSlice, func(age string) int {
intAge, _ := strconv.Atoi(age)
return intAge
})
fmt.Printf("用户年龄累加结果: %d\n", sum)
执行上述代码,打印结果如下:
用户年龄累加结果: 60
计算结果和之前一样,看起来代码实现比之前的简单迭代更复杂了,但是代码复用性、可读性和后续可维护性更好,毕竟,对于长期维护的项目而言,业务代码不可能一次编写好就完事了。目前来看,只要是符合上述约定参数类型的切片数据,现在都可以通过这段代码来实现指定字段值的累加功能,并且支持自定义字段和数值类型转化逻辑。
引入 Filter 函数
有的时候,为了让 Map-Reduce 代码更加健壮(排除无效的字段值),或者只对指定范围的数据进行统计计算,还可以在 Map-Reduce 基础上引入 Filter(过滤器),对集合元素进行过滤。
我们在上面的代码中新增一个 Filter 函数:
func itemsFilter(items []map[string]string, f func(map[string]string) bool) []map[string]string {
newSlice := make([]map[string]string, len(items))
for _, item := range items {
if f(item) {
newSlice = append(newSlice, item)
}
}
return newSlice
}
接下来,我们可以在 main 函数中应用 Filter 函数对无效用户年龄进行过滤,或者排除指定范围年龄:
func main() {
var users = []map[string]string{
{
"name": "张三",
"age": "18",
},
{
"name": "李四",
"age": "22",
},
{
"name": "王五",
"age": "20",
},
{
"name": "赵六",
"age": "-10",
},
{
"name": "孙七",
"age": "60",
},
{
"name": "周八",
"age": "10",
},
}
//fmt.Printf("用户年龄累加结果: %d\n", ageSum(users))
validUsers := itemsFilter(users, func(user map[string]string) bool {
age, ok := user["age"]
if !ok {
return false
}
intAge, err := strconv.Atoi(age)
if err != nil {
return false
}
if intAge <18 || intAge > 35 {
return false
}
return true
})
ageSlice := mapToString(validUsers, func(user map[string]string) string {
return user["age"]
})
sum := fieldSum(ageSlice, func(age string) int {
intAge, _ := strconv.Atoi(age)
return intAge
})
fmt.Printf("用户年龄累加结果: %d\n", sum)
执行上述代码,打印结果如下:
用户年龄累加结果: 60
通过管道重构 Map-Reduce-Filter 代码
package main
import (
"log"
)
type user struct {
name string
age int
}
func filterAge(users []user) interface{} {
var slice []user
for _, u := range users {
if u.age >= 18 && u.age <= 35 {
slice = append(slice, u)
}
}
return slice
}
func mapAgeToSlice(users []user) interface{} {
var slice []int
for _, u := range users {
slice = append(slice, u.age)
}
return slice
}
func sumAge(users []user, pipes ...func([]user) interface{}) int {
var ages []int
var sum int
for _, f := range pipes {
result := f(users)
switch result.(type) {
case []user:
users = result.([]user)
case []int:
ages = result.([]int)
}
}
if len(ages) == 0 {
log.Fatalln("没有在管道中加入 mapAgeToSlice 方法")
}
for _, age := range ages {
sum += age
}
return sum
}
func main() {
var users = []user{
{
name: "张三",
age: 18,
},
{
name: "李四",
age: 22,
},
{
name: "王五",
age: 20,
},
{
name: "赵六",
age: -10,
},
{
name: "孙七",
age: 60,
},
{
name: "周八",
age: 10,
},
}
sum := sumAge(users, filterAge, mapAgeToSlice)
log.Printf("用户年龄累加结果: %d\n", sum)
}
执行上述代码,打印结果如下:
2021/03/09 09:29:43 用户年龄累加结果: 60
这里,我们引入了一个 user
结构体替代字典类型,让代码更加简洁,可读性更好
然后我们将 Filter
和 Map
函数中的闭包函数取消掉了,改为直接在代码中实现,以便精简代码,为了便于通过管道统一声明 Filter
和 Map
函数,将他们的返回值声明成了空接口 interface{}
表示可以返回任何类型。
接下来重点来看 Reduce
函数 sumAge
的实现,这里,我们将其第二个参数声明为了变长参数类型,表示支持传递多个处理函数,这些处理器函数按照声明的先后顺序依次调用,由于这些处理函数的返回值类型被声明为了空接口,所以需要在运行时动态对它们的返回值类型做检测,并赋值给指定变量,以便程序可以按照我们期望的路径执行下去,而不会因为类型问题报错退出(这是一个简单版的 Go 泛型实现):
for _, f := range pipes {
result := f(users)
switch result.(type) {
case []user:
users = result.([]user)
case []int:
ages = result.([]int)
}
}
最后一个处理函数的结果 ages 整型切片将作为 Reduce
函数求和逻辑的数据源。