为什么需要泛型
在 Go 1.18 之前,想要写“对任意类型都适用的函数”只有两条路:复制粘贴多份,或者用 interface{} 配合类型断言。前者臃肿难维护,后者在编译期丢失类型安全,运行时才可能 panic。
泛型(generics)用类型参数解决了这个矛盾:同一份逻辑,对多种类型都成立,同时保留编译期类型检查。
核心概念:类型集合与 ~T
泛型的基石是“类型集合”(type set)。约束(constraint)不再只是一个接口,而是一组允许的类型。最常见的写法是近似元素 ~T:
// ~int 表示“底层类型是 int 的所有类型”,包括自定义类型 type MyInt int
type Number interface {
~int | ~int64 | ~float64
}
标准库 constraints 包提供了 Ordered(可比较大小)、Integer、Float 等常用约束,comparable 则是语言内建的可比较约束。
实战一:泛型 Min/Max
用 constraints.Ordered 写出一个对任意可排序类型都成立的极值函数:
import "golang.org/x/exp/constraints"
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
调用时类型可自动推导:Min(3, 5)、Min("go", "rust") 都能编译通过,且返回类型与入参一致。
实战二:Slice 工具函数
泛型最实用的场景是通用容器操作。下面用三个函数实现 Map、Filter、Reduce:
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
func Filter[T any](s []T, f func(T) bool) []T {
var r []T
for _, v := range s {
if f(v) {
r = append(r, v)
}
}
return r
}
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
这三个函数对任意切片类型都适用,类型安全且零反射开销。例如把一组订单金额求和:
total := Reduce(orders, 0.0, func(acc float64, o Order) float64 {
return acc + o.Amount
})
何时不该用泛型
泛型不是银弹。如果只有一两种类型用到,直接写具体类型更清晰;当逻辑差异很大、只是“看起来像”时,用接口 + 多态反而更合适。一个经验法则:
当同一份逻辑要作用于多种类型、且类型之间行为一致时,才上泛型;否则优先具体类型或接口。
小结
泛型让 Go 在保持简洁的同时获得了表达力:类型集合与 ~T 定义约束,constraints 包提供常用集合,Slice 工具函数是最高频的落地点。用对地方,代码既能复用又不失类型安全。
