为什么 Go 长期没有泛型
Go 早期刻意不做泛型:官方觉得"简单够用就行",复制粘贴比抽象成模板更直接。但写得久了,你会发现大量重复——写一个对 int 切片求最大值的函数,再来一份对 float64 的,再来一份对 string 的。Go 1.18 终于补上泛型,让你写一份函数适用于多种类型。
与此同时,Go 的错误处理却一直保持着"老派"风格:没有 try/catch,错误靠多返回值显式返回、由调用方显式检查。这两件事放一起看特别有意思——它一边拥抱现代抽象,一边坚持把错误处理写在明面上。
Go 的设计哲学:宁可显式重复,也不要隐藏控制流;宁可多打几行代码,也别让错误被吞掉。
- [T any]类型参数的基本写法,T 是占位类型。
- 约束用 interface 限定 T 必须具备哪些能力。
- comparable内置约束,表示 T 能用 == 比较。
- 多返回值函数同时返回结果和 error。
- errors.Is判断错误链里是不是包含某个特定错误。
类型参数:给函数加一个"占位类型"
泛型函数在函数名后面加一对中括号,里面写上类型参数列表。下面这个 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 顺着包的链条一路找上去——这在分层调用里特别有用。
实战:泛型安全除法
把泛型和错误处理串起来:写一个对任意数值类型做除法的函数,除零要返回错误,而不是让程序崩掉。
import "errors"
type Number interface {
int | float64
}
func Divide[T Number](a, b T) (T, error) {
if b == 0 {
var zero T
return zero, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
r1, err := Divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Println(r1)
r2, err := Divide(5.0, 0.0)
if err != nil {
fmt.Println("捕获到:", err) // 捕获到: 除数不能为零
}
}
同一份 Divide 逻辑,对 int 和 float64 都能用;除零错误被显式返回,由调用方决定怎么处理。这就是 Go"泛型复用 + 显式错误"组合起来的日常写法。
泛型让你少写重复代码,多返回值让错误无处隐藏——Go 把"复用"和"诚实"这两件事都摆在了明面上。