主题
05 泛型与类型参数
一句话:泛型让同一段算法在编译期对多种类型复用,约束规定「哪些类型可以进来、能用哪些操作」;它换掉了接口装箱和运行时断言,代价是约束设计和可读性,不是所有重复都值得泛型化。
适用 Go 1.18+;slices/maps 泛型工具从 1.21 起进入标准库,cmp.Ordered 也从 1.21 起提供。
为什么需要
没有泛型时,通用容器只能用 any 加类型断言,丢类型安全还要装箱;或者为每种类型复制一份代码。泛型把类型检查提前到编译期,同时保留具体类型的运算方式。但 Go 的泛型是约束式的,不是 C++ 模板:没有特化,也没有模板元编程,约束写不出来的时候就该退回接口或具体类型。
核心机制
类型参数与约束
go
func Sum[T ~int | ~int64 | ~float64](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}T 是类型参数,方括号里是约束。~int 表示「底层类型是 int 的具名类型也算」,所以 type MyInt int 可以传进来;不加 ~ 只匹配 int 本身。联合类型用 | 连接,约束也可以同时要求方法和类型集合,具体规则以官方文档为准。
comparable 是预定义约束,允许 == 和 !=,覆盖数值、string、指针、可比较的 struct/数组;slice、map、func 不满足。需要排序时用 cmp.Ordered(1.21+),不要自己重复定义。
类型推断会从实参推 T,例如 Sum([]int{1, 2, 3});推不出来就显式写 Sum[int](xs)。
泛型函数与泛型类型
go
type Pair[K comparable, V any] struct {
Key K
Val V
}
func Keys[K comparable, V any](m map[K]V) []K泛型函数适合算法(Sum、Map、Filter),泛型类型适合容器(Pair、Set、Stack)。方法不能自己新增类型参数,func (p Pair[K, V]) Convert[U any]() 不合法;需要新类型参数就写成泛型函数。泛型类型的方法接收者要重复类型参数:func (p Pair[K, V]) Get() V。
什么时候不该用
- 只有一种具体类型:直接写具体类型,泛型只增加阅读成本。
- 行为差异大于结构相似:用接口表达行为,不要用类型分支把泛型写成 switch。
- 需要访问字段:约束只能约束方法和类型集合,不能约束字段;用访问器方法或接口。
- 公共 API:泛型签名会暴露约束,收紧约束可能破坏调用方;先在小范围使用。
- 性能没验证:泛型不保证比接口快,编译器的实现策略属于内部细节,用 benchmark 比较。
最小上手
go
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
type MyInt int
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
type Pair[K comparable, V any] struct {
Key K
Val V
}
func Keys[K comparable, V any](m map[K]V) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func main() {
fmt.Println(Sum([]int{1, 2, 3})) // 6:从实参推断 T
fmt.Println(Sum([]MyInt{1, 2})) // 3:~int 接受底层类型
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
p := Pair[string, int]{Key: "age", Val: 30}
fmt.Println(p, Keys(map[string]int{"a": 1}))
}完整版本见 examples/05_generics/main.go。运行:go run examples/05_generics/main.go。
生产动作
- 约束靠近使用点:定义在包内、放在使用它的函数附近;优先复用
comparable、cmp.Ordered和标准库约束。 - 约束尽量小:需要
==才用comparable,需要排序才用cmp.Ordered;不要图省事写any再断言。 - 优先标准库泛型:
slices.Sort/Contains/Clone/Equal、maps.Clone/Equal已经覆盖大部分容器操作。 - 泛型类型封装内部切片:提供
Clone、迭代方法,不要直接暴露字段让调用方改内部状态。 - 检查:
go vet、benchmark 看allocs/op,go doc看签名是否可读;复杂约束加注释和示例。
边界
- 没有特化和模板元编程,不能按类型生成不同实现。
- 约束不能表达字段,也不能表达约束之外的运算符;复杂需求回到接口或具体类型。
- 泛型方法不能新增类型参数;泛型类型的方法要重复声明类型参数。
- 编译后的实现策略(代码生成或形状共享)是内部细节,不要依赖代码体积和性能假设。
- 递归约束、复杂联合类型会显著降低可读性,宁可多写一个具体函数。
- 泛型不能替代接口做运行时多态:类型在编译期就确定了。
讲清楚
泛型解决的是「同一套逻辑对多种类型复用,同时保留类型安全」。类型参数写在方括号里,约束描述允许的类型集合和可用操作:comparable 允许相等比较,~int 表示底层类型是 int,联合类型把多个条件并起来。调用时编译器从实参推断类型,推不出来再显式指定。它适合算法和容器,不适合行为差异大、需要访问字段或只有一种类型的场景。取舍是用约束换掉接口装箱和运行时断言,但约束本身会增加签名复杂度;写不出来就说明这里不该用泛型。
适用 Go 1.18+(基线 1.22) | 初稿 2026-10