Go 泛型入门

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

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 上不能做类型断言。
  • 重复三份再泛型化,能用标准库就不自己写。

下一步