为什么 Go 长期没有泛型

Go 早期刻意不做泛型:官方觉得"简单够用就行",复制粘贴比抽象成模板更直接。但写得久了,你会发现大量重复——写一个对 int 切片求最大值的函数,再来一份对 float64 的,再来一份对 string 的。Go 1.18 终于补上泛型,让你写一份函数适用于多种类型。

与此同时,Go 的错误处理却一直保持着"老派"风格:没有 try/catch,错误靠多返回值显式返回、由调用方显式检查。这两件事放一起看特别有意思——它一边拥抱现代抽象,一边坚持把错误处理写在明面上。

Go 的设计哲学:宁可显式重复,也不要隐藏控制流;宁可多打几行代码,也别让错误被吞掉。
本文核心知识点
  • [T any]类型参数的基本写法,T 是占位类型。
  • 约束用 interface 限定 T 必须具备哪些能力。
  • comparable内置约束,表示 T 能用 == 比较。
  • 多返回值函数同时返回结果和 error。
  • errors.Is判断错误链里是不是包含某个特定错误。
Go 泛型与错误处理配图:深蓝渐变背景,中央一个发光的通用模具方框,模具内部透出方块、圆柱、三角等多种不同形状的发光几何体从模具中穿出,周围有连接管道与齿轮装饰
一个通用模具,穿出多种形状——这正是泛型"一份逻辑、多种类型"的直观比喻。

类型参数:给函数加一个"占位类型"

泛型函数在函数名后面加一对中括号,里面写上类型参数列表。下面这个 Max 函数对"任意可比较类型"都能取最大值:

func Max[T any](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Max(3, 7))          // 7
    fmt.Println(Max("go", "rust"))   // rust
    fmt.Println(Max(1.5, 2.3))      // 2.3
}

[T any] 表示 T 是一个类型参数,any 是它的约束(意思是"任意类型都行")。调用时不用显式传类型,编译器自己根据参数推断。

约束:限定类型参数能干什么

不是所有类型都能 > 比较。比如你不能比较两个 map 谁大。Go 用约束告诉编译器"T 必须具备哪些能力",否则那行代码编译不过。

// 内置约束 comparable:T 必须能用 == 和 != 比较
func IndexOf[T comparable](list []T, target T) int {
    for i, v := range list {
        if v == target {
            return i
        }
    }
    return -1
}

你也可以自定义约束:定义一个 interface,里面列出 T 必须实现的方法。泛型函数在 T 上调用这些方法时,编译器才允许。

泛型容器:写一个通用栈

泛型最常见的用武之地是容器:一个栈既能装 int、也能装 string,不用每种类型写一份。结构体也能加类型参数。

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(x T) {
    s.items = append(s.items, x)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

用的时候指定一下类型即可:s := Stack[int]{} 是 int 栈,s2 := Stack[string]{} 是 string 栈。栈的逻辑只写一份。

Go 的错误处理:多返回值

Go 不用异常。一个可能失败的函数,通常返回两个值:结果和一个 error。调用方必须显式检查 error 是不是 nil。

f, err := os.Open("hello.txt")
if err != nil {
    // 打开失败,处理它
    log.Println("打不开:", err)
    return
}
defer f.Close()
// 正常用 f

看起来啰嗦,但好处是:每一处可能出错的地方都明明白白写在代码里,不会突然冒一个异常上来打乱流程。

自定义错误与 errors.Is / As

用 errors.New 或 fmt.Errorf 创建错误。Go 1.13 之后支持错误"包装",再用 errors.Is 和 errors.As 在包装过的错误链里找特定错误。

var ErrNotFound = errors.New("找不到记录")

func load(id string) error {
    return fmt.Errorf("加载 %s 失败: %w", id, ErrNotFound)
}

func main() {
    err := load("abc")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("这是找不到的错误")
    }
}

%w 把底层错误包进新错误里,errors.Is 顺着包的链条一路找上去——这在分层调用里特别有用。