从切片工具函数学习泛型最自然
Go 1.18 有了泛型后,最容易上手的练习就是切片工具函数。因为切片处理在业务代码里非常常见:过滤活跃用户、提取 ID、计算总金额、把数据库结果转成响应结构体。过去这些函数要么针对具体类型写,要么使用 interface{} 损失类型安全。泛型让它们变得更自然。
这篇文章手写 Filter、Map 和 Reduce。它不是为了鼓励你把所有业务代码都写成函数式链条,而是通过简单例子理解类型参数如何传递。
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)
}
}
测试多个类型能帮助你确认泛型函数没有偷偷依赖某个具体类型。虽然编译器会检查类型约束,但业务语义仍然需要测试保护。
小结
Filter、Map 和 Reduce 是理解 Go 泛型切片处理的好练习。它们展示了类型参数如何在输入、输出和函数参数之间传递,也展示了 any 约束适合没有额外操作要求的场景。
真正写业务时,要根据可读性选择。简单转换用泛型工具很清楚,复杂流程用普通循环更稳。泛型让你多了选择,不是让你放弃 Go 的清楚风格。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。