利用Go语言泛型特性开发通用算法库(ForEach, Filter, Map, Reduce)

我们开发程序时,经常要对数据进行各种处理。如果用数学语言对数据处理操作进行抽象,可以归纳成以下操作:

  • AnyMatch ,判断是否有数据满足判定条件;
  • AllMatch ,判断是否所有数据均满足判定条件;
  • ForEach ,遍历每个数据并执行指定处理函数;
  • Filter ,过滤出满足判定条件的数据;
  • Map ,根据指定转换函数逐个转换数据;
  • Reduce ,对数据进行合并,最终计算出一个结果(比如累加);
  • etc

背景

堆砌代码

在没有泛型之前,对于每种不同的数据类型,这些处理函数都要单独实现一遍。以 Filter 为例,假设我们有一些用户数据需要过滤,可以为其实现 Filter 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type User struct {
	Id   string
	Name string
	Age  int
	// ...
}

type Users []*User

func (users Users) Filter(filter func(*User) bool) Users {
	result := make(Users, 0, len(users))
	for _, user := range users {
		if filter(user) {
			result = append(result, user)
		}
	}
	return result
}

假设现在我们又需要处理一些配置数据,只能重新针对新数据类型写一个新的 Filter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Config struct {
	Id  string
	Key string
	// ...
}

type Configs []*Config

func (configs Configs) Filter(filter func(*Config) bool) Configs {
	result := make(Configs, 0, len(configs))
	for _, config := range configs {
		if filter(config) {
			result = append(result, config)
		}
	}
	return result
}

请看这两个 Filter 方法的代码,除了类型不同,其他都是一模一样的!然而就是因为类型不同,我们无法复用代码,只能简单堆砌。

如果每种数据类型都要赋值这些通用的代码,工作量肯定不小,想想都头大!这就是 Go 社区对泛型呼声很高的原因。好在千呼万唤总算出来了!

泛型优化

Go1.18 版本开始引入泛型特性,支持定义类型参数,让一份代码处理任意类型变成可能。请看这段代码,这是一个通用 Filter 函数版本,它支持处理任意数据类型:

1
2
3
4
5
6
7
8
9
func Filter[Data any, Datas ~[]Data](datas Datas, filter func(Data) bool) Datas {
	result := make(Datas, 0, len(datas))
	for _, data := range datas {
		if filter(data) {
			result = append(result, data)
		}
	}
	return result
}

Filter 后面的方括号是类型参数,其中:

  • Data 是数据元素的类型,any 表示可以是任意类型;
  • Datas 是数据切片的类型,~[]Data 表示实现了 Data 切片接口的所有类型;

Filter 的参数有两个,其中:

  • datas 是待过滤数据,它是 Datas 类型,即由 Data 元素组成的切片;
  • filter 是一个判定函数,根据元素 Data 返回 true/false 决定该元素是否包含;

这样一来,这个 Filter 既可以用来处理用户数据 Users

1
2
3
4
5
6
7
var users Users
// ...

// 过滤出所有小于10岁的儿童
Filter[*User, Users](users, func(u *User) bool {
    return u.Age < 10
})

又可以用来处理配置数据 Configs ,相当灵活:

1
2
3
4
5
6
7
var configs Configs
// ...

// 过滤出所有包含fasionchan.com的配置
Filter[*Config, Configs](configs, func(c *Config) bool {
	return strings.Contains(c.Key, "fasionchan.com")
})

你可能觉得调用泛型函数必须指定类型参数,太过麻烦。好在 Go 支持 类型推断 ,我们可以省略它:

1
2
3
Filter(configs, func(c *Config) bool {
	return strings.Contains(c.Key, "fasionchan.com")
})

Go 一看调用的泛型函数,便开始思考:

  • 第一个参数的类型是 Configs ,因此类型参数 Datas 一定是 Configs
  • Configs*Config 的切片,因此类型参数 Data 一定是 *Config

顺便再提一下,我看网上很多博主都是这样设计泛型函数:

1
2
3
4
5
6
7
8
9
func Filter1[Data any](datas []Data, filter func(Data) bool) []Data {
	result := make([]Data, 0, len(datas))
	for _, data := range datas {
		if filter(data) {
			result = append(result, data)
		}
	}
	return result
}

这个版本的 Filter 只有一个类型参数,表示数据元素的类型,而数据切片的类型则是 Data 的直接切片。虽然大部分场景下使用这个版本的 Filter 也可以,但某些场景下却不够灵活。

举个例子,我经常对切片类型进行自定义,添加一些工具函数方便使用:

1
2
3
4
5
type Users []*User

func (users Users) DoSomething() {
  // ...
}

如果用 Filter1 对数据进行过滤,得到的结果是 []*User 类型,而不是 Users 类型:

1
2
3
4
fmt.Printf("%T\n", Filter1(users, func(u *User) bool {
	return true
}))
// 输出:[]*User

这时无法接着调用定义在 Users 上的方法,只能先做类型转换,而第一个 Filter 就没有这个问题:

1
2
3
4
fmt.Printf("%T\n", Filter(users, func(u *User) bool {
	return true
}))
// 输出:Users

泛型算法库

AnyMatch

遍历数据每个元素,测试看是否有数据满足指定条件:

1
2
3
4
5
6
7
8
func AnyMatch[Data any](datas []Data, test func(Data) bool) bool {
	for _, data := range datas {
		if test(data) {
			return true
		}
	}
	return false
}
  • datas ,待处理数据;
  • test ,判定函数,以数据元素为参数,返回布尔值;

AllMatch

遍历数据每个元素,测试看是否全部数据均满足指定条件:

1
2
3
4
5
6
7
8
func AllMatch[Data any](datas []Data, test func(Data) bool) bool {
	for _, data := range datas {
		if !test(data) {
			return false
		}
	}
	return true
}
  • datas ,待处理数据;
  • test ,判定函数,以数据元素为参数,返回布尔值;

ForEach

遍历数据每个元素,并以其为参数调用指定处理函数:

1
2
3
4
5
func ForEach[Data any](datas []Data, handler func(data Data)) {
	for _, data := range datas {
		handler(data)
	}
}
  • datas ,待处理数据;
  • handler ,处理函数,以数据元素为参数,没有返回值;

Filter

遍历数据每个元素,从中过滤出满足指定判定条件的元素:

1
2
3
4
5
6
7
8
9
func Filter[Data any, Datas ~[]Data](datas Datas, filter func(Data) bool) Datas {
	result := make(Datas, 0, len(datas))
	for _, data := range datas {
		if filter(data) {
			result = append(result, data)
		}
	}
	return result
}
  • datas ,待处理数据;
  • filter ,判定函数,以数据元素为参数,返回布尔值决定是否过滤出来;
1
2
3
4
5
6
7
8
// 应用实例:过滤出奇数
var testNumbers = []int{1, 2, 3}

var odd = func(i int) bool {
	return i%2 == 1
}

fmt.Println(Filter(testNumbers, odd)) // 输出:[1 3]

Map

遍历数据每个元素,调用转换函数进行转换:

1
2
3
4
5
6
7
func Map[Data any, Datas ~[]Data, Result any](datas Datas, mapper func(Data) Result) []Result {
	results := make([]Result, 0, len(datas))
	for _, data := range datas {
		results = append(results, mapper(data))
	}
	return results
}
  • datas ,待处理数据;
  • mapper ,转换函数,以数据元素为参数,返回转换结果;
1
2
3
4
5
6
7
8
// 应用实例:对数列整体乘以2
var testNumbers = []int{1, 2, 3}

var doubler = func (i int) int {
  return i * 2
}

fmt.Println(Map(testNumbers, doubler)) // 输出:[2, 4, 6]

Reduce

1
2
3
4
5
6
7
func Reduce[Data any, Result any](datas []Data, reducer func(Data, Result) Result, initial Result) (result Result) {
	result = initial
	for _, data := range datas {
		result = reducer(data, result)
	}
	return
}
  • datas ,待处理数据;
  • reducer ,合并函数,将当前元素与上一个结果合并起来并返回;
  • initial ,结果初始值;
1
2
3
4
5
6
7
8
// 应用实例:对数列进行求和
var testNumbers = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

var accumulator = func(current int, previous int) int {
	return current + previous
}

fmt.Println(Reduce(testNumbers, accumulator, 0)) // 输出:45

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: