Go 泛型切片工具入门:自己写 Filter、Map 和 Reduce

本文用 Go 1.18 泛型手写 Filter、Map 和 Reduce,帮助初学者理解泛型在集合处理中的真实使用方式和边界。

从切片工具函数学习泛型最自然

Go 1.18 有了泛型后,最容易上手的练习就是切片工具函数。因为切片处理在业务代码里非常常见:过滤活跃用户、提取 ID、计算总金额、把数据库结果转成响应结构体。过去这些函数要么针对具体类型写,要么使用 interface{} 损失类型安全。泛型让它们变得更自然。

这篇文章手写 FilterMapReduce。它不是为了鼓励你把所有业务代码都写成函数式链条,而是通过简单例子理解类型参数如何传递。

Filter:保留符合条件的元素

func Filter[T any](items []T, keep func(T) bool) []T {
	result := make([]T, 0, len(items))
	for _, item := range items {
		if keep(item) {
			result = append(result, item)
		}
	}
	return result
}

使用:

type User struct {
	Name   string
	Active bool
}

users := []User{
	{Name: "小林", Active: true},
	{Name: "阿周", Active: false},
}

activeUsers := Filter(users, func(user User) bool {
	return user.Active
})

fmt.Println(activeUsers)

Filter 不关心 T 是什么类型,只把每个元素交给 keep 判断。这里 any 足够。

Map:把元素转换成另一种类型

func Map[T any, R any](items []T, convert func(T) R) []R {
	result := make([]R, 0, len(items))
	for _, item := range items {
		result = append(result, convert(item))
	}
	return result
}

提取用户名:

names := Map(users, func(user User) string {
	return user.Name
})

提取 ID:

type Article struct {
	ID    int64
	Title string
}

ids := Map(articles, func(article Article) int64 {
	return article.ID
})

T 是输入类型,R 是输出类型。编译器会根据调用自动推断,大多数时候你不需要手写类型参数。

Reduce:把列表折叠成一个值

func Reduce[T any, R any](items []T, initial R, combine func(R, T) R) R {
	result := initial
	for _, item := range items {
		result = combine(result, item)
	}
	return result
}

计算订单总金额:

type Order struct {
	ID         int64
	TotalCents int64
}

total := Reduce(orders, int64(0), func(sum int64, order Order) int64 {
	return sum + order.TotalCents
})

统计状态:

counts := Reduce(orders, map[string]int{}, func(acc map[string]int, order Order) map[string]int {
	acc[order.Status]++
	return acc
})

这个例子可以工作,但要注意 map 是引用类型,combine 修改的是同一个 map。这样写没错,但调用者要理解副作用。很多时候普通循环更直观:

counts := make(map[string]int)
for _, order := range orders {
	counts[order.Status]++
}

泛型工具不是必须替代所有循环。

链式调用要克制

你可以写:

names := Map(Filter(users, func(user User) bool {
	return user.Active
}), func(user User) string {
	return user.Name
})

这能运行,但可读性未必比普通循环好:

var names []string
for _, user := range users {
	if !user.Active {
		continue
	}
	names = append(names, user.Name)
}

Go 社区一直偏爱直接控制流。泛型工具适合减少重复,但不要为了追求“表达式化”让调试和阅读变难。

给工具函数写测试

泛型函数也应该测试。比如 Filter

func TestFilter(t *testing.T) {
	got := Filter([]int{1, 2, 3, 4}, func(n int) bool {
		return n%2 == 0
	})

	want := []int{2, 4}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("got %v, want %v", got, want)
	}
}

再测字符串:

func TestFilterString(t *testing.T) {
	got := Filter([]string{"Go", "", "PHP"}, func(s string) bool {
		return s != ""
	})

	want := []string{"Go", "PHP"}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("got %v, want %v", got, want)
	}
}

测试多个类型能帮助你确认泛型函数没有偷偷依赖某个具体类型。虽然编译器会检查类型约束,但业务语义仍然需要测试保护。

小结

FilterMapReduce 是理解 Go 泛型切片处理的好练习。它们展示了类型参数如何在输入、输出和函数参数之间传递,也展示了 any 约束适合没有额外操作要求的场景。

真正写业务时,要根据可读性选择。简单转换用泛型工具很清楚,复杂流程用普通循环更稳。泛型让你多了选择,不是让你放弃 Go 的清楚风格。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页