跳到正文

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

把一门技术讲透到既懂也会用