Go 泛型入门
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
Go 1.18 加入泛型,1.21 又补了 cmp、slices、maps 等标准库。泛型不是「到处都能用」的银弹,它解决的是同一套逻辑要适配多种类型、且不想牺牲类型安全的场景。
没有泛型时的两种写法
// 1) 每种类型写一遍
func SumInts(xs []int) int { /* ... */ }
func SumFloats(xs []float64) float64 { /* ... */ } // 还会越来越多
// 2) 用 any + 类型断言
func SumAny(xs []any) any {
total := 0
for _, x := range xs {
total += x.(int) // 运行期才炸
}
return total
}
泛型把类型变成编译期参数,既不重复也不丢检查。
第一个泛型函数
func Sum[T int | float64](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
T是类型参数,int | float64是它的约束(constraint),+能用的原因就在这里。- 调用时通常不用写
Sum[int](...),编译器能从实参推断。 var total T拿到该类型的零值,这是泛型代码里唯一「安全」的初始化方式。
内置约束
func Contains[T comparable](xs []T, target T) bool {
for _, x := range xs {
if x == target {
return true
}
}
return false
}
any:任意类型(interface{}的别名)。只能做赋值、传参、==之外的少量操作。comparable:可用==/!=比较,map 的 key 也要求它。切片、map、函数不满足。- 其他能力(算术、排序)需要自定义约束。
自定义约束
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
func Average[T Number](xs []T) float64 {
if len(xs) == 0 {
return 0
}
var sum T
for _, x := range xs {
sum += x
}
return float64(sum) / float64(len(xs))
}
~ 表示「底层类型是它」的自定义类型也算:
type Celsius float64
Average([]Celsius{20.5, 21.5}) // 合法
不加 ~ 只接受字面类型本身,自定义类型会被拒绝——这是定义约束时最容易忽略的细节。
标准库 cmp.Ordered 已经覆盖可比较大小的全部基础类型:
import "cmp"
func Max[T cmp.Ordered](xs []T) T {
m := xs[0]
for _, x := range xs[1:] {
if x > m {
m = x
}
}
return m
}
约束里可以有方法
约束是接口,可以同时要求方法:
type Stringer interface {
String() string
}
func Join[T Stringer](xs []T, sep string) string {
parts := make([]string, len(xs))
for i, x := range xs {
parts[i] = x.String()
}
return strings.Join(parts, sep)
}
也可以组合:
type Key interface {
comparable
String() string
}
泛型类型
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
var s Stack[string]
s.Push("a")
注意:方法不能引入新的类型参数。func (s *Stack[T]) Map[U any](f func(T) U) 是非法的;需要转换时写成独立的泛型函数:
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
names := Map(users, func(u User) string { return u.Name })
Filter、Reduce 同理。标准库 slices 已提供 slices.Contains、slices.Index、slices.Sort 等常用操作,先看有没有现成的。
类型推断
Sum([]int{1, 2}) // 推断 T=int
Map(users, func(u User) string { return u.Name }) // 推断 T=User, U=string
推断不出来的情况:约束来自返回值、或类型参数只出现在返回值里。
func Zero[T any]() T { var z T; return z }
Zero[int]() // 必须显式指定
什么时候不该用泛型
- 只有一种类型会用:直接写具体类型,可读性更好。
- 需要的行为差异很大:用接口 + 多态,别用一堆类型分支。
- 为了省几行代码:泛型会让调用处的错误信息变长,维护成本上升。
- 需要动态类型:
any+ 断言或反射更合适(如 JSON 处理)。
经验法则:同一段逻辑出现第三份复制时再考虑泛型。
常见错误与注意事项
1. 约束太宽,操作不可用
func Add[T any](a, b T) T { return a + b } // 编译错误:operator + not defined
any 不保证任何运算,改用 Number 这类约束。
2. 忘了 ~
自定义类型 type UserID int64 无法满足 int64 约束,加 ~int64 才行。
3. 对 T 做类型断言
func Print[T any](v T) {
if s, ok := v.(string); ok { ... } // 非法:T 不是接口值
}
需要按类型分支时,说明这里其实不该用泛型;或者把 T 约束到接口,用接口方法。
4. 泛型方法
Go 不支持方法级类型参数,只能给类型或函数加。
5. nil 与零值
var zero T 不一定是 nil(T=int 时是 0)。需要区分「未设置」时用 *T 或 (T, bool)。
6. 过度抽象
Repository[T any, ID comparable, ...] 这种签名,读的人要先解方程。参数超过两个就重新想想。
7. 忽略标准库
slices、maps、cmp 已经覆盖排序、查找、去重、比较。自己写之前先查文档。
小结
- 类型参数让同一逻辑适配多种类型,约束决定能做什么操作。
any无能力,comparable可比较,cmp.Ordered可排序;自定义约束用~纳入底层类型。- 泛型类型可以有方法,但方法不能新增类型参数。
- 值只能用
var zero T初始化;T上不能做类型断言。 - 重复三份再泛型化,能用标准库就不自己写。