go语言设计模式(Map-Reduce)

我理解的设计模式

说到设计模式,多数人想到的都是 1995 年 GoF 提出的 23 种设计模式 他通常被定义为:

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。

从定义上看,设计模式其实是一种经验的总结,是针对特定问题的简洁而优雅的解决方案。我认为只要是优雅的,简洁的,可进行多业务场景问题解决的,易于理解使用的一套行之有效的代码编写模式都可以称为设计模式或是编程模式。

既然是经验总结的编程模式,那么学习它的最直接的好处就在于可以站在巨人的肩膀上解决软件开发过程中的一些特定问题。

我写编程模式这个模块也是为了将好的模式收集积累,在遇到相似的问题场景,可以拿来主义。哈哈哈,听起来有些不动脑子的愚蠢。但你想想我们这个世界不就是这样在前人的肩膀上一步步的发展进步的。你如果做什么都要自己思考,总结,设计都要从0到1这本身就不太现实,即使是你做到了,那也一定不够优雅或是错误百出。

设计模式这个大标签下,我将把我阅读他人总结的好的设计模式(想看全部设计模式只要打开设计模式的标签就好),阅读项目源码发现的好的设计模式结合自己的想法和解释进行积累总结以提升我的代码质量。值得注意的是再好的设计模式也不要在不理解他的情况下生搬硬套。

设计模式积累(一):Map-Reduce

这个设计模式是我在极客时间大神左耳朵耗子的《Go语言编程模式实战》中看到的。感觉不错,就加上自己的理解把模式收录在这里:

Map-Reduce 理解起来其实很简单,一句话:他只是一种控制逻辑,真正的业务逻辑是以传给它们的数据和函数来定义的。是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。

设计模式示例

  • Map示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}
  • Reduce示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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)

  • Filter示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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其实是就像go中的map原语一样把数组汇总每一个值都映射成了另一个值,然后返出新的数组。

Reduce故名思义就是进行数量,尺寸,价格等一些属性或字段的统计

Filter也一样,过滤掉不符合条件的

所以 Map,Reduce,Filter 其实编码模式的设计思想是一样的,但控制方式不同,命名也是根据他的控制方式来进行的。

业务示例

建立员工信息

我们有一个员工对象和一些数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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 函数

我们有下面的几个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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
}

我从控制逻辑和业务逻辑的角度描述这几个方法,让你更好的理解设计思想:

EmployeeCountIf : 控制函数统计并输出某个业务逻辑下的员工总数

EmployeeFilterIn : 控制函数过滤并输出满足某个业务逻辑的员工列表

EmployeeSumIf:控制函数统计并输出某个属性的总和

各种自定义的统计示例于是,我们就可以有接下来的代码了。

  • 统计有多少员工大于 40 岁
1
2
3
4
5
old := EmployeeCountIf(list, func(e *Employee) bool {
return e.Age > 40
})
fmt.Printf("old people: %d\n", old)
//old people: 2
  • 列出有没有休假的员工
1
2
3
4
5
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}]
  • 统计 30 岁以下员工的薪资总和
1
2
3
4
5
6
younger_pay := EmployeeSumIf(list, func(e *Employee) int {
if e.Age < 30 {
return e.Salary
}
return 0
})

泛型 Map-Reduce

刚刚的 Map-Reduce 都因为要处理数据的类型不同,而需要写出不同版本的 Map-Reduce,虽然它们的代码看上去是很类似的。

我们一开始说了设计模式一定是通用的,可以处理多种业务场景的,仅仅是在某类型数组及对应的业务函数类型下才能使用,我觉得并不能称之为设计模式,所以接下来的优化后的代码才满足我收录它的条件。

所以,这里就要提到泛型编程了。1.16 前的 Go 语言的泛型只能用 interface{} + reflect来完成。interface{} 可以理解为 C 中的 void*、Java 中的 Object ,reflect是 Go 的反射机制包,作用是在运行时检查类型。下面,我们来看一下,一个非常简单的、不做任何类型检查的泛型的 Map 函数怎么写。

1
2
3
4
5
6
7
8
9
10
11

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)}获得数据。

于是,我们就可以有下面的代码——不同类型的数据可以使用相同逻辑的Map()代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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{"a", "b", "c"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[A B C]

健壮版的 Generic Map

所以,如果要写一个健壮的程序,对于这种用interface{} 的“过度泛型”,就需要我们自己来做类型检查。来看一个有类型检查的 Map 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

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
}

代码一下子就复杂起来了,可见,复杂的代码都是在处理异常的地方。

我来列一下代码中的几个要点。代码中没有使用 Map 函数,因为和数据结构有含义冲突的问题,所以使用Transform,这个来源于 C++ STL 库中的命名。有两个版本的函数,一个是返回一个全新的数组 Transform(),一个是“就地完成” TransformInPlace()。

在主函数中,用 Kind() 方法检查了数据类型是不是 Slice,函数类型是不是 Func。检查函数的参数和返回类型是通过 verifyFuncSignature() 来完成的:NumIn()用来检查函数的“入参”;NumOut() :用来检查函数的“返回值”。如果需要新生成一个 Slice,会使用 reflect.MakeSlice() 来完成。好了,有了这段代码,我们的代码就很可以很开心地使用了:

  • 可以用于字符串数组:
1
2
3
4
5
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"}
  • 可以用于整形数组:
1
2
3
4
5
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}
  • 可以用于结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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 代码如下(同样分是否“就地计算”的两个版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

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

要注意的是:使用反射来做这些东西会有一个问题,那就是代码的性能会很差。所以,上面的代码不能用在需要高性能的地方。