错误处理:没有 try-catch 的世界,error、panic、recover 三件套
错误处理:没有 try-catch 的世界,error、panic、recover 三件套
这一章讲 Go 最有辨识度的设计:没有 try-catch,错误就是普通的返回值。 函数把
error放在最后一个返回值传回来;你不检查,编译器不拦你, 但出了问题也是你自己的事。真正的崩溃只有一种:panic——而连 panic 都能被 defer + recover 捞回来。读完你会拿到 Go 错误处理的三层完整图景。
1. 错误的日常形态:一个普通的返回值
书里开篇就把 Go 和别的语言划清界限:别的语言用 try-catch / try-except, Go 没有异常;错误要么靠多返回值处理,要么靠 panic1。
先看一个真实的错误长什么样——打开一个不存在的文件:
file, err := os.Open("./example.json")
// 文件不存在时:
// file → <nil>(没有文件句柄)
// err → open ./example.json: The system cannot find the the file specified.
// 文件存在时:
// file → &{0x1185c420}(句柄)
// err → <nil>规矩就一句:err 不等于 nil 就是出错了;等于 nil 就是成功2。 这条规矩全书已经见过雏形(第 1 章的对照表里就有),这里是它的完全体。
「错误是返回值」意味着:错误的处理权在调用方,不在抛出方。
没有异常向上穿透,没有「忘了 catch 就崩」——代价是你要自己写 if err != nil。
这就是第 1 章说过的那条账单:「错误摆在明面上」的明面。
2. error 是一个接口:谁实现 Error(),谁就是错误
把第 12 章的接口知识用上,书里给了 error 的定义:
type error interface {
Error() string
}error 是一个只有一个方法的接口;任何类型,只要实现了
Error() string,它就是一个合法的 error 值3。
书里给的最小自制错误:
type My_error struct{}
func (e *My_error) Error() string {
return "Unexpected outcome"
}
fmt.Println(&My_error{}) // → Unexpected outcome没有字段的空结构体,只靠一个方法,就造出了一个错误类型3。
日常不用自己造这么原始的东西——标准库 errors 包的 New 函数
一行造出带消息的 error:
error_msg := errors.New("file operation failed")
// 类型是 *errors.errorString:内部一个小结构体装着消息字符串,
// 实现了 Error() 方法书里点了一个值得知道的实现细节:errors.New 返回的类型是
*errors.errorString,内部结构就是一个字符串字段 + Error 方法4。
判断(我们的,不是书里的): 「error 是接口」这个事实,解释了 Go 错误体系 为什么能长到今天这个规模:任何人都能造自己的错误类型(带字段、带方法、 互相包裹)。书里只教了 errors.New,真实项目里你会见到
fmt.Errorf加上下文、errors.Is/As判错误的身份—— 它们全都建立在「error 是接口」这一个事实上。 如果错,会错在: 接口意味着动态分发和堆分配,极高频的错误路径上 这是有成本的(Go 社区为此做过多年优化,比如 error 的懒格式化提案)—— 但那属于性能调优层,不改变机制。
3. defer 的完整规矩(第 6 章欠的账,这里结清)
书里在错误处理的语境下,把 defer 的规矩补全成了五条5:
- defer 把函数压栈,外层函数返回时按后进先出(LIFO)执行;
- defer 语句的参数,在它出现那一刻就求值,不是执行时才算;
- defer 函数里读、写都可以;
- defer 能改命名返回值(下面专门演示);
- panic 时 defer 照跑——这是 recover 存在的物理基础。
规则 2 的反面教材,书里给了:把 defer file.Close() 写在 os.Open 之前,
编译错 undefined: file——defer 记账时 file 还不存在6。
正确姿势反过来:先打开,立刻 defer 关:
func open_file() {
file, err := os.Open("./example.txt")
defer file.Close() // 放在打开成功之后
...
}规则 4 的例子很妙——defer 里改命名返回值:
func update_return_value() (i int) {
defer func() {
i = i + 10 // return 之后才执行,但还能改 i
}()
return 1 // 先给 i 赋 1,然后 defer 把它改成 11
}
// → updated return value: 11图说:return 1 不是「拍板走人」,是「先把结果存进 i,等 defer 们折腾完,
带着最终的 i 走人」。裸返回 + defer 改值,是 Go 里最「魔法」的一块,
看懂这个例子就看懂了它的全部边界7。
LIFO 的样子(循环里压栈 0~10 个 defer,倒着出:10, 9, 8, …, 0)8。
4. 主线例子:一个除零,走通 panic → recover 全链
本章主线,照原书的 op(10, 0)(a 除以 b),四步走完全部善后机制。
第一站:不处理——运行时 panic。
func op(a, b int) int {
result = a / b
return result
}
result = op(10, 0)
// → panic: runtime error: integer divide by zeropanic 发生时,程序立即终止——后面的 Completed 打不出来9。
除以零、数组越界这类运行时错误,底层都是 panic。
第二站:主动 panic,带上自己的消息。
if b == 0 {
panic("Can't divide a number by zero!")
}
// → panic: Can't divide a number by zero!panic 的参数是空接口,任何值都能传;作用是「这局面我没法继续了,
谁都别拦,往上崩」10。
第三站:panic 之后,defer 照样跑。
func op(a, b int) int {
defer op_defer() // 记账
if b == 0 {
panic("Can't divide a number by zero!")
}
...
}
func main() {
defer main_defer() // 也记账
result = op(10, 0)
...
} op defer function executed
main defer function executed
panic: Can't divide a number by zero!图说:panic 不是「立刻死」,是「当前函数停止,沿调用栈向上滚, 一路把每层的 defer 执行完,然后进程退出」11。 这就是为什么善后代码(关文件、还锁)必须写在 defer 里——只有它保证跑得到。
第四站:recover——把崩溃捞回来。
func op(a, b int) int {
defer func() {
if r := recover(); r != nil { // 有 panic 可捞
fmt.Println("Panic recovered", r) // r = panic 传的那个值
op_defer()
}
}()
if b == 0 {
panic("Can't divide a number by zero!")
}
...
} Panic recovered Can't divide a number by zero!
op defer function executed
Completed ← 程序没有死,继续往下走!
main defer function executedrecover 的规矩,书里说得清楚:它只能在 defer 里调(panic 之后只有
defer 还活着);没有 panic 时它返回 nil;panic 时它返回 panic 传的那个值,
并且——调用 recover 之后,panic 停止,程序从 panic 点恢复,接着跑12。
这就是 Go 版的 try-catch:panic ≈ 抛,defer+recover ≈ 兜。 但书里没有(也不应该)鼓励你把它当日常机制——Go 社区的规矩: 预期内的错误用 error 返回值,recover 只留给「真没想到」的兜底 (比如 HTTP 服务器防止单个请求崩掉整个进程)。
5. 三层错误处理的各自领地
把全章收成一张分工表:
| 层 | 机制 | 用在哪 |
|---|---|---|
| 预期内 | error 返回值 + if err != nil | 文件打不开、网络断了、输入不合法——日常 99% |
| 预期外 | panic | 程序内部一致性被破坏(不变量失效)、程序员自己的断言失败 |
| 兜底 | defer + recover | 进程级保险:服务器每个请求包一层,单个请求崩了不拖死整个服务 |
判断(我们的,不是书里的): 这一章的设计哲学可以压成一句: Go 把「错误的可见性」置于「错误的省事性」之上。 Java 的受检异常想让编译器逼你处理,结果人人写空 catch; Go 干脆不逼,把检查错的责任和权力一起交给你——于是有了 「库的作者诚实返回、调用方显式检查」这套文化(以及
_丢弃错误的 少数派报告)。这不是技术优劣,是价值排序。 如果错,会错在: 如果某个生态(比如 Google 内部早期)事实上大量 用 panic/recover 当控制流(程序执行的先后顺序),那「预期外用 panic」的规矩 就是事后追认的 理想,不是实践——但从公开代码看,error 返回值的统治地位是真实的。
6. 边界与局限
- 没有讲错误的包装与判定:
fmt.Errorf("...: %w", err)给错误加上下文、errors.Is/As判断错误身份——真实项目的每日功课,本书止于 errors.New; - 没有讲自定义错误类型的最佳实践(字段、哨兵错误 sentinel error);
- panic/recover 的演示是教学级的最小形态,真实用法要配合命名返回值 把「崩了但没有死」传回给调用方,书里未展开;
- defer 的性能开销(每次压栈都有成本)未提——高频路径上有人说 defer 太贵, 属于微优化讨论(补充,不在书里)。
7. 可带走的
- Go 没有 try-catch;错误 = 函数最后一个返回值,err != nil 即出错;
- error 是接口:
Error() string;谁实现它,谁就是错误; errors.New("消息")一行造错误;内部是一个装字符串的小结构体;- defer 参数当场求值——先打开资源,再 defer 关;
- defer 后进先出;多个 defer 从最后记的账开始还;
- defer 能改命名返回值:return 先存值,defer 再改,带着最终值走人;
- panic = 停止当前函数,沿栈向上滚,一路执行 defer,然后进程死;
- panic 参数是空接口,任何值都行;
- recover 只能在 defer 里调;捞到 panic 的值、程序原地复活;
- 分工:预期内用 error 返回值,预期外才 panic,recover 只做兜底保险。
8. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| Go 无异常、错误两途 | Error Handling | text/25-fm-learning-go-programming.txt:52(搜「does not have exceptions」) |
| os.Open 的错误形态 | Error Handling | text/25-fm-learning-go-programming.txt:97(搜「file status: <nil>」) · text/25-fm-learning-go-programming.txt:112(搜「compare the last return value」) |
| error 接口定义 | Error Handling | text/25-fm-learning-go-programming.txt:118(搜「single method Error」) · text/25-fm-learning-go-programming.txt:127(搜「Error() string」) |
| 自制错误类型 | Error Handling | text/25-fm-learning-go-programming.txt:169(搜「Unexpected outcome」) |
| errors.New | Error Handling | text/25-fm-learning-go-programming.txt:211(搜「built-in package called errors」) · text/25-fm-learning-go-programming.txt:268(搜「errorString」) |
| defer 五条规矩 | Error Handling | text/25-fm-learning-go-programming.txt:274(搜「deferred until the surrounding function returns」) · text/25-fm-learning-go-programming.txt:280(搜「last in the first out order」) |
| defer 在打开之前(undefined: file) | Error Handling | text/25-fm-learning-go-programming.txt:340(搜「undefined: file」) |
| LIFO 循环演示 | Error Handling | text/25-fm-learning-go-programming.txt:439(搜「value: 10」) |
| defer 改命名返回值(11) | Error Handling | text/25-fm-learning-go-programming.txt:514(搜「updated return value: 11」) |
| 运行时 panic(除零) | Error Handling | text/25-fm-learning-go-programming.txt:571(搜「integer divide by zero」) |
| 主动 panic 带消息 | Error Handling | text/25-fm-learning-go-programming.txt:622(搜「Can't divide a number by zero」) |
| panic 时 defer 照跑 | Error Handling | text/25-fm-learning-go-programming.txt:763(搜「op defer function executed main defer function executed」) |
| recover 复活程序 | Error Handling | text/25-fm-learning-go-programming.txt:772(搜「controls the panicking situation」) · text/25-fm-learning-go-programming.txt:859(搜「Panic recovered」) · text/25-fm-learning-go-programming.txt:868(搜「resumes the execution」) |
Footnotes
-
出处:「Error Handling」第 52 段(
text/25-fm-learning-go-programming.txt:52,搜「does not have exceptions」)。 ↩ -
出处:「Error Handling」第 82–112 段(
text/25-fm-learning-go-programming.txt:97,搜「file status: <nil>」;text/25-fm-learning-go-programming.txt:100,搜「cannot find the file」;text/25-fm-learning-go-programming.txt:112,搜「compare the last return value」)。 ↩ -
出处:「Error Handling」第 118–133 段(
text/25-fm-learning-go-programming.txt:118,搜「single method Error」;text/25-fm-learning-go-programming.txt:127,搜「Error() string」)。 ↩ ↩2 -
出处:「Error Handling」第 208–268 段(
text/25-fm-learning-go-programming.txt:211,搜「built-in package called errors」;text/25-fm-learning-go-programming.txt:268,搜「errorString」)。 ↩ -
出处:「Error Handling」第 271–292 段(五条规矩见第 280–292 段)。 ↩
-
出处:「Error Handling」第 295–343 段(
text/25-fm-learning-go-programming.txt:340,搜「undefined: file」;text/25-fm-learning-go-programming.txt:343,搜「does not get the file object」)。 ↩ -
出处:「Error Handling」第 472–514 段(
text/25-fm-learning-go-programming.txt:490,搜「i = i+10」;text/25-fm-learning-go-programming.txt:514,搜「updated return value: 11」)。 ↩ -
出处:「Error Handling」第 400–469 段(
text/25-fm-learning-go-programming.txt:439,搜「value: 10」;text/25-fm-learning-go-programming.txt:469,搜「value: 0」)。 ↩ -
出处:「Error Handling」第 517–574 段(
text/25-fm-learning-go-programming.txt:571,搜「integer divide by zero」;text/25-fm-learning-go-programming.txt:574,搜「terminated immediately」)。 ↩ -
出处:「Error Handling」第 577–625 段(
text/25-fm-learning-go-programming.txt:622,搜「Can't divide a number by zero」)。 ↩ -
出处:「Error Handling」第 628–766 段(
text/25-fm-learning-go-programming.txt:763,搜「op defer function executed main defer function executed」;text/25-fm-learning-go-programming.txt:766,搜「panic: Can't divide」)。 ↩ -
出处:「Error Handling」第 769–868 段(
text/25-fm-learning-go-programming.txt:772,搜「controls the panicking situation」;text/25-fm-learning-go-programming.txt:859,搜「Panic recovered」;text/25-fm-learning-go-programming.txt:862,搜「Completed」)。 ↩