学完这篇你能用 Go 泛型写出一组真正能复用的类型安全容器和工具函数,并且知道哪些事泛型做不到、别硬套。
第一步:认准类型参数的形状
Go 1.18 起支持类型参数,写法是函数名后的方括号:
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 5) // 类型推断,T = int
Max[float64](1, 2.5) // 显式指定
几点命名习惯:T、K、V 是社区通用,别起 Type1 这种。调用时能推断就别显式写方括号,代码更干净。
注意:Go 不做隐式数值转换。Max(1, 2.5) 推断不出统一的 T,会直接编译报错,必须显式写成 Max[float64](1, 2.5)。
第二步:用 interface 定义可复用约束
把 int | float64 抽出来:
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs {
s += x
}
return s
}
这里的 ~ 是关键:它表示「底层类型是 int 的所有类型」,所以 type UserID int 也能用。去掉 ~ 后只认完全等于 int 的类型,自定义类型会编译失败。
注意:写库给别人用的时候,几乎总是应该加 ~。自己项目里只用一个具体类型,加不加都行。
第三步:认清三个现成约束
any:等价 interface{},什么都能传,但函数体里除了赋值、比较到 nil 之外什么都干不了。
comparable:可用 ==、!=,也是 map 的 key 要求。想用 var zero T; if v == zero 判断零值,就必须有这个约束。
cmp.Ordered(Go 1.21+,包 cmp):包含所有整数、浮点、字符串,配合 < > 使用。
第四步:常用模式一——集合操作函数
func Map[T, R any](xs []T, f func(T) R) []R {
out := make([]R, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
func Filter[T any](xs []T, keep func(T) bool) []T {
out := make([]T, 0, len(xs))
for _, x := range xs {
if keep(x) {
out = append(out, x)
}
}
return out
}
注意 make 里用的不是 make([]T, len(xs)) 而是长度 0、容量 len——否则会多出一堆零值元素。这是新手最容易写错的一处。
第五步:常用模式二——泛型容器
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](vs ...T) Set[T] {
s := make(Set[T], len(vs))
for _, v := range vs {
s[v] = struct{}{}
}
return s
}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
用 struct{} 做值而不是 bool,省内存也表意清楚。类型参数写在类型名后面,方法上用 Set[T] 即可,不需要重复声明 T。
注意:方法不能有自己的类型参数。想给 Set 加个转换方法 func (s Set[T]) ToSlice[R any](...) 是编译不过的,只能写成包级函数 func ToSlice[T, R any](s Set[T], f func(T) R) []R。设计 API 时要提前避开这个坑。
第六步:常用模式三——泛型结构体 + 约束组合
type Pair[K comparable, V any] struct {
Key K
Val V
}
type Repository[T any, ID comparable] struct {
items map[ID]T
}
约束里也可以嵌接口方法,比如 type Stringer interface { String() string },用来约束「必须能格式化输出」的类型。
第七步:别踩这几个坑
- 函数体里
new(T) 只能拿到 *T,想构造值用 var zero T。
- 约束是
any 时不能对值做算术、不能取字段、不能调用方法,否则换回具体类型或加约束。
- 泛型不能替代接口多态:需要运行时动态行为,还是用 interface。
- 泛型不带来性能提升,Go 用的是 GC shape stenciling,别指望它能优化热路径。
- 参数只有一两个具体类型时,直接写两遍更快更可读,别为了「通用」上泛型。
小结
- 约束的本质是「哪些操作合法」,
Number 里有 ~int 才能做 +,comparable 才能做 ==。
~ 决定自定义类型能不能进来,对外发布的约束默认加。
- 高频模式就三类:
Map/Filter 这类切片工具、Set[T comparable] 这类容器、Pair[K,V] 这类结构体。
- 最大的限制是方法不能带类型参数,容器设计要么拆成包级函数,要么别做链式调用。
- 泛型适合「同一套逻辑套多种类型」,不适合「运行时才知道类型」——后者仍然是 interface 的地盘。