title: Implementing Function Chaining with Pipeline Technology
date: 2022-07-04 10:40:00
toc: false
index_img: http://api.btstu.cn/sjbz/?lx=m_dongman&cid=10
category:
- Go
tags: - Sequential
- Chaining
- Support
- Request
- Method
- Function
The term "Pipeline" originates from Unix Shell command line, where we can use the pipeline operator | to combine simple commands and achieve powerful functionality. For example, if we want to filter out the nginx process from the system process list, we can do it like this:
ps -ef | grep nginx
Here, we use the pipeline to connect the basic Unix commands ps
and grep
, and filter out the nginx process from the result of ps -ef
.
In functional programming, we can also use the concept of pipelines to chain simple functions and build more powerful functionality. The most common example is the use of function chaining, which is similar to the flow interface pattern in object-oriented programming and allows for chained processing.
This way, each function can focus on its own task and do it to the best of its ability. Then, by combining them using pipelines, we can build more complex business functionality. This also aligns with the SOLID design principle of single responsibility.
In our main
function, we use pipelines to combine the Map-Reduce-Filter
functional modules and achieve a chained function call:
package main
import (
"fmt"
"log"
)
type user struct {
name string
age int
}
func main() {
var users = []user{
{
name: "xmp",
age: 11,
},
{
name: "wlm",
age: 24,
},
{
name: "wjf",
age: 30,
},
{
name: "lm",
age: 50,
},
}
// The processor functions are called in the order they are declared
result := sumAge(users, filterAge, mapAgeToSlice)
fmt.Println("Total age (excluding ages greater than 40 and less than 20):", result)
}
func filterAge(users []user) interface{} {
var result []user
for _, user := range users {
if user.age < 40 && user.age > 20 {
result = append(result, user)
}
}
return result
}
func mapAgeToSlice(users []user) interface{} {
var result []int
for _, user := range users {
result = append(result, user.age)
}
return result
}
func sumAge(users []user, pips ...func([]user) interface{}) (result int) {
var reusltSlice []int
for _, f := range pips {
runResult := f(users)
switch runResult.(type) {
case []user:
users = runResult.([]user)
case []int:
reusltSlice = runResult.([]int)
}
}
if len(reusltSlice) == 0 {
log.Fatalln("The mapAgeToSlice method is not added to the pipeline")
}
for _, age := range reusltSlice {
result += age
}
return result
}
Here, we introduce a user
struct to replace the dictionary type, making the code more concise and readable. The details about struct types will be explained in the next chapter on Go type system.
Then, we remove the closure functions in the Filter
and Map
functions and implement them directly in the code to simplify it. To facilitate the unified declaration of Filter
and Map
functions through pipelines, we declare their return values as empty interface interface{}
, which can represent any type.
Next, let's focus on the implementation of the Reduce
function sumAge
. Here, we declare its second parameter as a variadic parameter, indicating support for passing multiple processing functions. These processor functions are called in the order they are declared. Since the return value types of these functions are declared as empty interfaces, we need to dynamically check their return value types at runtime and assign them to the specified variables, so that the program can execute as expected without errors due to type issues.
Through pipelines, we can achieve a more elegant flow of Filter->Map->Reduce
function chaining. In addition, pipeline technology is widely used in HTTP request handling middleware.