我们开发程序时,经常要对数据进行各种处理。如果用数学语言对数据处理操作进行抽象,可以归纳成以下操作:
- 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 社区对泛型呼声很高的原因。好在千呼万唤总算出来了!
泛型优化
Go 从 1.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语言】系列文章首发于公众号【小菜学编程】,敬请关注: