Go Generics and Type Parameters(Go 泛型与类型参数)是一份关于在 Go 语言中使用泛型的技能资料。
适用条件
泛型需要 Go 1.18 及以上版本。当需要判断是否使用 Go 泛型、编写泛型函数或类型、选择约束,或在类型别名与类型定义之间取舍时,可参考本资料。
资源路由
references/CONSTRAINTS.md:在组合约束、使用类型集,或在泛型与接口之间选择时阅读。
何时使用泛型
基本原则:先使用具体类型,只有当第二种类型出现时才进行泛化。
优先使用泛型的场景:
- 多种类型共享完全相同的逻辑(如排序、过滤、map/reduce)。
- 否则需要依赖 any 和大量类型切换。
- 正在构建可复用的数据结构(如并发安全集合、有序 map)。
避免使用泛型的场景:
- 实践中只会实例化一种类型。
- 接口已经能清晰地建模共享行为。
- 泛型代码比针对具体类型的替代方案更难阅读。
引用:“Write code, don't design types.” —— Robert Griesemer 与 Ian Lance Taylor。
决策流程
多种类型是否共享完全相同的逻辑?
- 否 → 使用具体类型
- 是 → 它们是否共享一个有用的接口?
- 是 → 使用接口
- 否 → 使用泛型
示例(过早泛型,实际只用 int 调用):
func Sum[T constraints.Integer | constraints.Float](vals []T) T { var total T for _, v := range vals { total += v } return total }
推荐写法:
func SumInts(vals []int) int { var total int for _, v := range vals { total += v } return total }
类型参数命名
T:通用类型参数;K:map 键类型;V:map 值类型;E:元素/条目类型。
对于复杂约束,也可使用简短描述性名称,例如:
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
类型别名与类型定义
类型别名(type Old = new.Name)较少使用,仅用于包迁移或渐进式 API 重构。
约束组合
使用 ~(底层类型)与 |(并集)组合约束:
type Numeric interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64 }
func Sum[T Numeric](vals []T) T { var total T for _, v := range vals { total += v } return total }
对于 cmp.Ordered 等标准约束,应使用 constraints 包或 cmp 包(Go 1.21+),而不是自行编写。
常见误区
不要包装标准库类型:
// 不好:泛型包装增加复杂度却无价值 type Set[T comparable] struct { m map[T]struct{} }
// 更好:用法简单时直接使用 map[T]struct{} seen := map[string]struct{}{}
当泛型能消除多个调用点的重复时,才值得其复杂度。单次使用的泛型只是间接层。
不要用泛型满足接口:
// 不好:T 只是用来满足接口 —— 直接用接口即可 func Process[T io.Reader](r T) error { ... }
// 好:直接接受接口 func Process(r io.Reader) error { ... }
避免过度约束:
// 不好:约束比需要的更严格 func Contains[T interface{ ~int | ~string }](slice []T, target T) bool { ... }
// 好:comparable 已足够 func Contains[T comparable](slice []T, target T) bool { ... }
快速参考
- 何时使用泛型:仅当多种类型共享完全相同的逻辑且接口不足够时。
- 起点:先写具体代码,之后再泛化。
- 命名:单个大写字母(T、K、V、E)。
- 类型别名:同一类型的替代名称;仅用于迁移。
- 约束组合:使用 ~ 表示底层类型,使用 | 表示并集。
- 常见误区:不要泛化单次使用的代码,或在接口足够时使用泛型。
相关技能
- 接口与泛型:在判断接口是否已能建模共享行为而无需泛型时,参见 go-interfaces。
- 类型声明:在定义新类型、类型别名,或在类型定义与别名之间选择时,参见 go-declarations。
- 记录泛型 API:在为泛型函数编写文档注释和可运行示例时,参见 go-documentation。
- 命名类型参数:在为类型参数或约束接口选择名称时,参见 go-naming。