Go语言:泛型(Generics)初体验 (Go 1.18)

Go 1.18 终于带来了社区期盼已久的泛型支持。这篇文章记录我实际使用 Go 泛型的体验,包括类型参数、约束定义以及几个实用场景。

泛型语法基础

Go 泛型通过 类型参数 (type parameters) 实现,语法上在函数名或类型名后用方括号声明:

func Map[T any, R any](s []T, f func(T) R) []R {
    result := make([]R, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

any 是 Go 1.18 新增的内建约束,等价于 interface{},表示接受任何类型。

类型约束 (Constraints)

约束本质上是接口,定义了类型参数必须满足的条件。Go 1.18 引入了一种新的接口语法,允许在接口中列出类型集合:

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

~int 表示底层类型是 int 的所有类型(包括 type MyInt int 这类自定义类型),这个波浪号语法是泛型提案里很关键的设计。

comparable 约束

comparable 是另一个内建约束,表示类型支持 ==!= 运算。典型用途是做 map 的 key:

func Contains[T comparable](s []T, target T) bool {
    for _, v := range s {
        if v == target {
            return true
        }
    }
    return false
}

func Unique[T comparable](s []T) []T {
    seen := make(map[T]bool)
    var result []T
    for _, v := range s {
        if !seen[v] {
            seen[v] = true
            result = append(result, v)
        }
    }
    return result
}

泛型类型

不只是函数,类型本身也可以带类型参数。一个常见例子是泛型链表或泛型集合:

type Set[T comparable] struct {
    m map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{m: make(map[T]struct{})}
}

func (s *Set[T]) Add(v T) {
    s.m[v] = struct{}{}
}

func (s *Set[T]) Has(v T) bool {
    _, ok := s.m[v]
    return ok
}

func (s *Set[T]) Len() int {
    return len(s.m)
}

使用起来很直观:

s := NewSet[string]()
s.Add("hello")
s.Add("world")
fmt.Println(s.Has("hello")) // true
fmt.Println(s.Len())        // 2

实际使用场景

泛型切片操作

标准库在 1.18 之后新增了 golang.org/x/exp/slices 包(后来进入了 1.21 标准库),但在此之前可以自己封装:

func Filter[T any](s []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range s {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T any, R any](s []T, init R, f func(R, T) R) R {
    acc := init
    for _, v := range s {
        acc = f(acc, v)
    }
    return acc
}

// 使用示例
nums := []int{1, 2, 3, 4, 5, 6, 7, 8}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })

泛型 Map 工具

处理 map[K]V 的通用函数以前只能用 interface{} 或代码生成,现在可以这样写:

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func Values[K comparable, V any](m map[K]V) []V {
    vals := make([]V, 0, len(m))
    for _, v := range m {
        vals = append(vals, v)
    }
    return vals
}

func MapValues[K comparable, V any, R any](m map[K]V, f func(V) R) map[K]R {
    result := make(map[K]R, len(m))
    for k, v := range m {
        result[k] = f(v)
    }
    return result
}

一些限制和注意事项

实际用下来,Go 泛型还是有一些限制的:

  1. 方法不能有额外类型参数。只有类型定义可以声明类型参数,方法只能使用所属类型已声明的参数。这意味着你不能给 Set[T] 加一个 Map[R]() []R 方法
  2. 没有特化 (specialization)。不能针对某个具体类型提供特殊实现
  3. 类型推断有限制。虽然大多数场景编译器能推断类型参数,但某些复杂情况需要显式指定
  4. 不支持在约束中使用方法和类型集合的交集。Go 1.18 的约束在表达能力上还是比 Rust trait 弱不少

总体来说,Go 泛型走的是一条务实路线。它不追求像 Rust/Haskell 那样强大的类型系统,而是解决 Go 开发者最常遇到的代码重复问题。对于写通用数据结构和工具函数来说,已经够用了。