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 泛型还是有一些限制的:
- 方法不能有额外类型参数。只有类型定义可以声明类型参数,方法只能使用所属类型已声明的参数。这意味着你不能给
Set[T]加一个Map[R]() []R方法 - 没有特化 (specialization)。不能针对某个具体类型提供特殊实现
- 类型推断有限制。虽然大多数场景编译器能推断类型参数,但某些复杂情况需要显式指定
- 不支持在约束中使用方法和类型集合的交集。Go 1.18 的约束在表达能力上还是比 Rust trait 弱不少
总体来说,Go 泛型走的是一条务实路线。它不追求像 Rust/Haskell 那样强大的类型系统,而是解决 Go 开发者最常遇到的代码重复问题。对于写通用数据结构和工具函数来说,已经够用了。