在本篇文章中,我們學習一下函數式編程的中非常重要的Map、Reduce、Filter的三種操作,這三種操作可以讓我們非常方便靈活地進行一些數據處理——我們的程序中大多數情況下都是在到倒騰數據,尤其對於一些需要統計的業務場景,Map/Reduce/Filter是非有通用的玩法。下面先來看幾個例子:
本文是全系列中第5 / 9篇:Go編程模式
- Go編程模式:切片,接口,時間和性能
- Go 編程模式:錯誤處理
- Go 編程模式:Functional Options
- Go編程模式:委託和反轉控制
- Go編程模式:Map-Reduce
- Go 編程模式:Go Generation
- Go編程模式:修飾器
- Go編程模式:Pipeline
- Go 編程模式:k8s Visitor 模式
目錄
基本示例
Map示例
下面的程序代碼中,我們寫了兩個Map函數,這兩個函數需要兩個參數,
- 一個是字符串數組
[]string
,說明需要處理的數據一個字符串 - 另一個是一個函數
func(s string) string
或func(s string) int
func MapStrToStr(arr []string, fn func(s string) string) []string {
var newArray = []string{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}
func MapStrToInt(arr []string, fn func(s string) int) []int {
var newArray = []int{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}
整個Map函數運行邏輯都很相似,函數體都是在遍歷第一個參數的數組,然後,調用第二個參數的函數,然後把其值組合成另一個數組返回。
於是我們就可以這樣使用這兩個函數:
var list = []string{"Hao", "Chen", "MegaEase"}
x := MapStrToStr(list, func(s string) string {
return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
//["HAO", "CHEN", "MEGAEASE"]
y := MapStrToInt(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", y)
//[3, 4, 8]
我們可以看到,我們給第一個 MapStrToStr()
傳了函數做的是 轉大寫,於是出來的數組就成了全大寫的,給MapStrToInt()
傳的是算其長度,所以出來的數組是每個字符串的長度。
我們再來看一下Reduce和Filter的函數是什麼樣的。
Reduce 示例
func Reduce(arr []string, fn func(s string) int) int {
sum := 0
for _, it := range arr {
sum += fn(it)
}
return sum
}
var list = []string{"Hao", "Chen", "MegaEase"}
x := Reduce(list, func(s string) int {
return len(s)
})
fmt.Printf("%v\n", x)
// 15
Filter示例
func Filter(arr []int, fn func(n int) bool) []int {
var newArray = []int{}
for _, it := range arr {
if fn(it) {
newArray = append(newArray, it)
}
}
return newArray
}
var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
return n%2 == 1
})
fmt.Printf("%v\n", out)
out = Filter(intset, func(n int) bool {
return n > 5
})
fmt.Printf("%v\n", out)
下圖是一個比喻,其非常形象地說明了Map-Reduce是的業務語義,其在數據處理中非常有用。
業務示例
通過上面的一些示例,你可能有一些明白,Map/Reduce/Filter只是一種控制邏輯,真正的業務邏輯是在傳給他們的數據和那個函數來定義的。是的,這是一個很經典的“業務邏輯”和“控制邏輯”分離解耦的編程模式。下面我們來看一個有業務意義的代碼,來讓大家強化理解一下什麼叫“控制邏輯”與業務邏輯分離。
員工信息
首先,我們一個員工對象,以及一些數據
type Employee struct {
Name string
Age int
Vacation int
Salary int
}
var list = []Employee{
{"Hao", 44, 0, 8000},
{"Bob", 34, 10, 5000},
{"Alice", 23, 5, 9000},
{"Jack", 26, 0, 4000},
{"Tom", 48, 9, 7500},
{"Marry", 29, 0, 6000},
{"Mike", 32, 8, 4000},
}
相關的Reduce/Fitler函數
然後,我們有如下的幾個函數:
func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {
count := 0
for i, _ := range list {
if fn(&list[i]) {
count += 1
}
}
return count
}
func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {
var newList []Employee
for i, _ := range list {
if fn(&list[i]) {
newList = append(newList, list[i])
}
}
return newList
}
func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {
var sum = 0
for i, _ := range list {
sum += fn(&list[i])
}
return sum
}
簡單說明一下:
EmployeeConutIf
和EmployeeSumIf
分別用於統滿足某個條件的個數或總數。它們都是Filter + Reduce的語義。EmployeeFilterIn
就是按某種條件過慮。就是Fitler的語義。
各種自定義的統計示例
於是我們就可以有如下的代碼。
1)統計有多少員工大於40歲
old := EmployeeCountIf(list, func(e *Employee) bool {
return e.Age > 40
})
fmt.Printf("old people: %d\n", old)
//old people: 2
2)統計有多少員工薪水大於6000
high_pay := EmployeeCountIf(list, func(e *Employee) bool {
return e.Salary >= 6000
})
fmt.Printf("High Salary people: %d\n", high_pay)
//High Salary people: 4
3)列出有沒有休假的員工
no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {
return e.Vacation == 0
})
fmt.Printf("People no vacation: %v\n", no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]
4)統計所有員工的薪資總和
total_pay := EmployeeSumIf(list, func(e *Employee) int {
return e.Salary
})
fmt.Printf("Total Salary: %d\n", total_pay)
//Total Salary: 43500
5)統計30歲以下員工的薪資總和
younger_pay := EmployeeSumIf(list, func(e *Employee) int {
if e.Age < 30 {
return e.Salary
}
return 0
})
泛型Map-Reduce
我們可以看到,上面的Map-Reduce都因爲要處理數據的類型不同而需要寫出不同版本的Map-Reduce,雖然他們的代碼看上去是很類似的。所以,這裏就要帶出來泛型編程了,Go語言在本文寫作的時候還不支持泛型(注:Go開發團隊技術負責人Russ Cox在2012年11月21golang-dev上的mail確認了Go泛型(type parameter)將在Go 1.18版本落地,即2022.2月份)。
簡單版 Generic Map
所以,目前的Go語言的泛型只能用 interface{}
+ reflect
來完成,interface{}
可以理解爲C中的 void*
,Java中的 Object
,reflect
是Go的反射機制包,用於在運行時檢查類型。
下面我們來看一下一個非常簡單不作任何類型檢查的泛型的Map函數怎麼寫。
func Map(data interface{}, fn interface{}) []interface{} {
vfn := reflect.ValueOf(fn)
vdata := reflect.ValueOf(data)
result := make([]interface{}, vdata.Len())
for i := 0; i < vdata.Len(); i++ {
result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()
}
return result
}
上面的代碼中,
- 通過
reflect.ValueOf()
來獲得interface{}
的值,其中一個是數據vdata
,另一個是函數vfn
, - 然後通過
vfn.Call()
方法來調用函數,通過[]refelct.Value{vdata.Index(i)}
來獲得數據。
Go語言中的反射的語法還是有點令人費解的,但是簡單看一下手冊還是能夠讀懂的。我這篇文章不講反射,所以相關的基礎知識還請大家自行Google相關的教程。
於是,我們就可以有下面的代碼——不同類型的數據可以使用相同邏輯的Map()
代碼。
square := func(x int) int {
return x * x
}
nums := []int{1, 2, 3, 4}
squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]
upcase := func(s string) string {
return strings.ToUpper(s)
}
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]
但是因爲反射是運行時的事,所以,如果類型什麼出問題的話,就會有運行時的錯誤。比如:
x := Map(5, 5)
fmt.Println(x)
上面的代碼可以很輕鬆的編譯通過,但是在運行時就出問題了,還是panic錯誤……
panic: reflect: call of reflect.Value.Len on int Value
goroutine 1 [running]:
reflect.Value.Len(0x10b5240, 0x10eeb58, 0x82, 0x10716bc)
/usr/local/Cellar/go/1.15.3/libexec/src/reflect/value.go:1162 +0x185
main.Map(0x10b5240, 0x10eeb58, 0x10b5240, 0x10eeb60, 0x1, 0x14, 0x0)
/Users/chenhao/.../map.go:12 +0x16b
main.main()
/Users/chenhao/.../map.go:42 +0x465
exit status 2
健壯版的Generic Map
所以,如果要寫一個健壯的程序,對於這種用interface{}
的“過度泛型”,就需要我們自己來做類型檢查。下面是一個有類型檢查的Map代碼:
func Transform(slice, function interface{}) interface{} {
return transform(slice, function, false)
}
func TransformInPlace(slice, function interface{}) interface{} {
return transform(slice, function, true)
}
func transform(slice, function interface{}, inPlace bool) interface{} {
//check the `slice` type is Slice
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic("transform: not slice")
}
//check the function signature
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 {
//Check it is a funciton
if fn.Kind() != reflect.Func {
return false
}
// NumIn() - returns a function type's input parameter count.
// NumOut() - returns a function type's output parameter count.
if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
return false
}
// In() - returns the type of a function type's i'th input parameter.
for i := 0; i < len(types)-1; i++ {
if fn.Type().In(i) != types[i] {
return false
}
}
// Out() - returns the type of a function type's i'th output parameter.
outType := types[len(types)-1]
if outType != nil && fn.Type().Out(0) != outType {
return false
}
return true
}
上面的代碼一下子就複雜起來了,可見,複雜的代碼都是在處理異常的地方。我不打算Walk through 所有的代碼,別看代碼多,但是還是可以讀懂的,下面列幾個代碼中的要點:
- 代碼中沒有使用Map函數,因爲和數據結構和關鍵有含義衝突的問題,所以使用
Transform
,這個來源於 C++ STL庫中的命名。 - 有兩個版本的函數,一個是返回一個全新的數組 –
Transform()
,一個是“就地完成” –TransformInPlace()
- 在主函數中,用
Kind()
方法檢查了數據類型是不是 Slice,函數類型是不是Func - 檢查函數的參數和返回類型是通過
verifyFuncSignature()
來完成的,其中:NumIn()
– 用來檢查函數的“入參”-
NumOut()
用來檢查函數的“返回值”
- 如果需要新生成一個Slice,會使用
reflect.MakeSlice()
來完成。
好了,有了上面的這段代碼,我們的代碼就很可以很開心的使用了:
可以用於字符串數組
list := []string{"1", "2", "3", "4", "5", "6"}
result := Transform(list, func(a string) string{
return a +a +a
})
//{"111","222","333","444","555","666"}
可以用於整形數組
list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {
return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}
可以用於結構體
var list = []Employee{
{"Hao", 44, 0, 8000},
{"Bob", 34, 10, 5000},
{"Alice", 23, 5, 9000},
{"Jack", 26, 0, 4000},
{"Tom", 48, 9, 7500},
}
result := TransformInPlace(list, func(e Employee) Employee {
e.Salary += 1000
e.Age += 1
return e
})
健壯版的 Generic Reduce
同樣,泛型版的 Reduce 代碼如下:
func Reduce(slice, pairFunc, zero interface{}) interface{} {
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic("reduce: wrong type, not slice")
}
len := sliceInType.Len()
if len == 0 {
return zero
} else if len == 1 {
return sliceInType.Index(0)
}
elemType := sliceInType.Type().Elem()
fn := reflect.ValueOf(pairFunc)
if !verifyFuncSignature(fn, elemType, elemType, elemType) {
t := elemType.String()
panic("reduce: function must be of type func(" + t + ", " + t + ") " + t)
}
var ins [2]reflect.Value
ins[0] = sliceInType.Index(0)
ins[1] = sliceInType.Index(1)
out := fn.Call(ins[:])[0]
for i := 2; i < len; i++ {
ins[0] = out
ins[1] = sliceInType.Index(i)
out = fn.Call(ins[:])[0]
}
return out.Interface()
}
健壯版的 Generic Filter
同樣,泛型版的 Filter 代碼如下(同樣分是否“就地計算”的兩個版本):
func Filter(slice, function interface{}) interface{} {
result, _ := filter(slice, function, false)
return result
}
func FilterInPlace(slicePtr, function interface{}) {
in := reflect.ValueOf(slicePtr)
if in.Kind() != reflect.Ptr {
panic("FilterInPlace: wrong type, " +
"not a pointer to slice")
}
_, n := filter(in.Elem().Interface(), function, true)
in.Elem().SetLen(n)
}
var boolType = reflect.ValueOf(true).Type()
func filter(slice, function interface{}, inPlace bool) (interface{}, int) {
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic("filter: wrong type, not a slice")
}
fn := reflect.ValueOf(function)
elemType := sliceInType.Type().Elem()
if !verifyFuncSignature(fn, elemType, boolType) {
panic("filter: function must be of type func(" + elemType.String() + ") bool")
}
var which []int
for i := 0; i < sliceInType.Len(); i++ {
if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {
which = append(which, i)
}
}
out := sliceInType
if !inPlace {
out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))
}
for i := range which {
out.Index(i).Set(sliceInType.Index(which[i]))
}
return out.Interface(), len(which)
}
後記
還有幾個未盡事宜:
1)使用反射來做這些東西,會有一個問題,那就是代碼的性能會很差。所以,上面的代碼不能用於你需要高性能的地方。怎麼解決這個問題,我們會在本系列文章的下一篇文章中討論。
2)上面的代碼大量的參考了 Rob Pike的版本,他的代碼在 https://github.com/robpike/filter
3)其實,在全世界範圍內,有大量的程序員都在問Go語言官方什麼時候在標準庫中支持 Map/Reduce,Rob Pike說,這種東西難寫嗎?還要我們官方來幫你們寫麼?這種代碼我多少年前就寫過了,但是,我從來一次都沒有用過,我還是喜歡用“For循環”,我覺得你最好也跟我一起用 “For循環”。
我個人覺得,Map/Reduce在數據處理的時候還是很有用的,Rob Pike可能平時也不怎麼寫“業務邏輯”的代碼,所以,對他來說可能也不太瞭解業務的變倫有多麼的頻繁……
當然,好還是不好,由你來判斷,但多學一些編程模式是對自己的幫助也是很有幫助的。
(全文完)
關注CoolShell微信公衆賬號和微信小程序
(轉載本站文章請註明作者和出處 酷 殼 – CoolShell ,請勿用於任何商業用途)