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
}

但集合操作的核心大多只依赖 anycomparable(可比较约束),因为我们需要判断相等性。

常用集合操作实现

以下所有工具函数均基于泛型编写,可直接用于生产环境。它们覆盖了日常开发中最常见的切片集合处理需求。

过滤 (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)
}

在上面代码中,FilterUnique 都直接复用,没有冗余的循环判断。这种风格提高了代码的表达力。

性能考量与注意事项

  1. 提前分配容量:在 Filter 等函数中,我们使用 make([]T, 0, len(slice)) 预分配容量,减少了动态扩容的开销。
  2. 对于大切片,避免不必要的复制:泛型函数返回新切片,内存分配是 O(n) 的。如果数据量极大且性能敏感,可以考虑原地修改或使用迭代器模式。
  3. comparable 的限制comparable 只能用于 ==!= 可比较的类型。像包含切片的结构体就不能满足 comparable,此时需要改用 ContainsByUniqueBy 这类自定义比较版本。
  4. 函数开销:泛型函数在编译期会生成具体类型的版本,运行时没有额外类型擦除成本,性能与手写具体类型的函数基本相同。
  5. 灵活使用组合约束:当需要同时对多个类型进行约束时,可以定义组合接口。例如:
type Ordered interface {
    constraints.Ordered
}