Press "Enter" to skip to content

Go 函数式编程篇(七):基于管道技术实现函数的流式调用

管道技术概述

管道(Pipeline)这一术语来源是 Unix 的 Shell 命令行,我们可以使用管道连接符 | 通过组合简单的命令实现强大的功能,比如我们想要从系统进程列表中筛选出 nginx 进程,可以这么做:

ps -ef | grep nginx 

这里就是通过管道连接了 psgrep 两个基本的 Unix 命令,在 ps -ef 的返回结果之上通过 grep nginx 筛选出 nginx 进程。

在函数式编程中,我们也可以借助管道的思想串联一些简单的函数构建更加强大的功能,比如最常见的流式函数调用(水流一样,在面向对象编程中对应的是流接口模式,可以实现链式处理)。

这样一来,每个函数就可以专注于自己要处理的事情,把它做到极致,然后通过组合方式(管道)构建更加复杂的业务功能,这也是符合 SOLID 设计原则的单一职责原则。

通过管道重构 Map-Reduce-Filter 代码

学院君在上篇教程中引入了 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
}

这里,我们引入了一个 user 结构体替代字典类型,让代码更加简洁,可读性更好,关于结构体类型,学院君将在下个章节 Go 类型系统中给大家详细介绍。

然后我们将 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 函数求和逻辑的数据源。

流式调用 Map-Reduce-Filter 函数

我们在 main 函数中通过管道组合 Map-Reduce-Filter 功能模块,实现这些函数的流式调用:

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)
}

可以看到,之前要写好几行函数调用代码才能获取计算结果,现在只需要一行代码就可以搞定:

sum := sumAge(users, filterAge, mapAgeToSlice)

通过管道,我们可以更优雅地实现 Filter->Map->Reduce 的流式调用。此外,管道技术在 HTTP 请求处理中间件中也有广泛的应用,后面我们介绍 Web 编程时会提到。

关于 Go 语言的函数式编程,学院君就简单介绍到这里,希望对你有所帮助和启发,下篇教程,我们将开始探索 Go 语言的类型系统和面向对象编程实现。

5 Comments

  1. wz8081633
    wz8081633 2021年12月7日

    sum := sumAge(users, filterAge, mapAgeToSlice)
    如果后面的两个函数调用顺序反了,结果会出问题…有啥优化的方法吗?大佬

    • klzdy123
      klzdy123 2022年2月20日

      如果担心人为调用顺序出错的话,可以在sumAge上套一层用于规范调用顺序的,但这样的话,管道就不能自由定义顺序了

发表回复