Go 泛型在集合操作中的应用
FreeGuideOnline
最新
2026-07-10
go func Filter[T any](slice []T, keep func(T) bool) []T
这里的 `T` 可以是任意类型,`any` 是 `interface{}` 的别名。
如果需要对数值类型求和,可以定义一个允许所有整数和浮点数的约束:
```go
import "golang.org/x/exp/constraints"
func Sum[T constraints.Ordered](nums []T) T {
var total T
for _, v := range nums {
total += v
}
return total
}
但集合操作的核心大多只依赖 any 或 comparable(可比较约束),因为我们需要判断相等性。
常用集合操作实现
以下所有工具函数均基于泛型编写,可直接用于生产环境。它们覆盖了日常开发中最常见的切片集合处理需求。
过滤 (Filter)
根据条件保留切片中的部分元素。
// Filter 保留所有使得 keep 返回 true 的元素。
func Filter[T any](slice []T, keep func(T) bool) []T {
result := make([]T, 0, len(slice))
for _, v := range slice {
if keep(v) {
result = append(result, v)
}
}
return result
}
使用示例:
numbers := []int{1, 2, 3, 4, 5}
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
// evens = [2, 4]
映射 (Map)
将切片中的每个元素转换为另一个类型(或相同类型)的值。
// Map 对切片中的每个元素应用 transform,返回新切片。
func Map[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
使用示例:
words := []string{"go", "泛型", "集合"}
lengths := Map(words, func(s string) int { return len(s) })
// lengths = [2, 6, 6] 中文字符按字节长度计算,具体取决于需求
包含判断 (Contains)
检查切片中是否存在某个元素。通常需要 comparable 约束,因为需要做 == 比较。
// Contains 判断 elem 是否在 slice 中。
func Contains[T comparable](slice []T, elem T) bool {
for _, v := range slice {
if v == elem {
return true
}
}
return false
}
如果需要自定义比较函数(例如结构体按某个字段比较),可以编写变体:
func ContainsBy[T any](slice []T, equal func(T) bool) bool {
for _, v := range slice {
if equal(v) {
return true
}
}
return false
}
去重 (Unique)
返回一个切片,其中每个元素只出现一次,保持首次出现的顺序。
// Unique 去除重复元素。
func Unique[T comparable](slice []T) []T {
seen := make(map[T]struct{}, len(slice))
result := make([]T, 0, len(slice))
for _, v := range slice {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
如果类型不满足 comparable,可以通过传递一个返回可比较键的函数来实现:
func UniqueBy[T any, K comparable](slice []T, key func(T) K) []T {
seen := make(map[K]struct{}, len(slice))
result := make([]T, 0, len(slice))
for _, v := range slice {
k := key(v)
if _, ok := seen[k]; !ok {
seen[k] = struct{}{}
result = append(result, v)
}
}
return result
}
查找 (Find)
返回第一个满足条件的元素及其所在索引。
// Find 返回第一个满足 match 的元素和其下标,未找到返回零值和 -1。
func Find[T any](slice []T, match func(T) bool) (T, int) {
for i, v := range slice {
if match(v) {
return v, i
}
}
var zero T
return zero, -1
}
求并集、交集、差集
这些集合操作依赖元素的可比较性。
// Union 返回两个切片的并集,元素唯一。
func Union[T comparable](a, b []T) []T {
return Unique(append(a, b...))
}
// Intersection 返回两个切片的交集,元素唯一。
func Intersection[T comparable](a, b []T) []T {
setA := make(map[T]struct{}, len(a))
for _, v := range a {
setA[v] = struct{}{}
}
result := make([]T, 0)
seen := make(map[T]struct{})
for _, v := range b {
if _, ok := setA[v]; ok {
if _, added := seen[v]; !added {
seen[v] = struct{}{}
result = append(result, v)
}
}
}
return result
}
// Difference 返回 a 中有而 b 中没有的元素。
func Difference[T comparable](a, b []T) []T {
setB := make(map[T]struct{}, len(b))
for _, v := range b {
setB[v] = struct{}{}
}
result := make([]T, 0)
for _, v := range a {
if _, ok := setB[v]; !ok {
result = append(result, v)
}
}
return result
}
实战:组合操作处理数据
泛型函数可以像搭积木一样组合使用,使数据处理逻辑变得清晰易读。
假设我们有一个用户列表,需要获取所有活跃用户的去重标签名称:
type User struct {
Active bool
Tags []string
}
func GetActiveUserTags(users []User) []string {
activeUsers := Filter(users, func(u User) bool { return u.Active })
allTags := make([]string, 0)
for _, u := range activeUsers {
allTags = append(allTags, u.Tags...)
}
return Unique(allTags)
}
在上面代码中,Filter 和 Unique 都直接复用,没有冗余的循环判断。这种风格提高了代码的表达力。
性能考量与注意事项
- 提前分配容量:在
Filter等函数中,我们使用make([]T, 0, len(slice))预分配容量,减少了动态扩容的开销。 - 对于大切片,避免不必要的复制:泛型函数返回新切片,内存分配是 O(n) 的。如果数据量极大且性能敏感,可以考虑原地修改或使用迭代器模式。
comparable的限制:comparable只能用于==和!=可比较的类型。像包含切片的结构体就不能满足comparable,此时需要改用ContainsBy、UniqueBy这类自定义比较版本。- 函数开销:泛型函数在编译期会生成具体类型的版本,运行时没有额外类型擦除成本,性能与手写具体类型的函数基本相同。
- 灵活使用组合约束:当需要同时对多个类型进行约束时,可以定义组合接口。例如:
type Ordered interface {
constraints.Ordered
}