错误处理
Go 没有 try / catch。日常约定是:
- 能预期的失败(参数不对、查无此人、除数为 0)→ 返回
error,由调用方决定怎么处理。 - 不该发生、程序状态已不可信(下标越界、断言失败、严重逻辑崩坏)→ 用
panic,必要时再在上层用defer+recover兜底。
本文用「九九乘法表」同一个业务场景,对比这两种写法的差别。
业务错误 vs 程序错误
| 类型 | 常见手段 | 出错后同函数里后面的代码 | 典型场景 |
|---|---|---|---|
| 业务错误 | 返回 (结果, error) | 还能继续走(只要你没 return) | 用户输入非法、业务校验失败 |
| 程序错误 | panic | 不再执行(即使 recover 了也是如此) | 严重异常、不可恢复状态 |
记住口诀:业务错误是「报告给调用方」;程序错误是「当前这条执行路径断了」。
业务错误:返回 error
error 是内置接口,只有一个方法 Error() string。没出错返回 nil,有错返回非 nil。
下面要求参数必须是 9 才打印九九表;传 8 时返回错误。注意:处理完错误之后,main 里的「其余逻辑」仍然会执行。
package main
import (
"errors"
"fmt"
)
// 业务校验失败:返回 error,不中断整个程序
func nn(a int) (bool, error) {
if a != 9 {
return false, errors.New("请输入9")
}
for i := 1; i <= a; i++ {
for k := 1; k <= i; k++ {
fmt.Printf("%d * %d = %d ", k, i, i*k)
}
fmt.Println()
}
return true, nil
}
func main() {
success, err := nn(8)
if err != nil {
fmt.Println(err, "执行失败")
} else {
fmt.Println(success, "执行成功")
}
// 业务错误只是「这次调用失败」,后面的代码照常走
fmt.Println("其余逻辑")
}
输出类似:
请输入9 执行失败
其余逻辑
要点:
- 用
errors.New("说明")或fmt.Errorf("a=%d 非法", a)创建错误。 - 调用方标准姿势:
if err != nil { ... }。 - 返回错误后,函数正常结束;调用方可以打日志、换参数重试,也可以继续干别的事。
程序错误:panic
把「请输入 9」改成 panic。一旦触发,从 panic 那一行开始,同函数里后面的代码不会再跑。
先看没有 recover 的情况——进程会直接崩溃:
package main
import "fmt"
func nn(a int) (bool, error) {
if a != 9 {
panic("请输入9") // 程序错误:直接中断当前执行路径
}
for i := 1; i <= a; i++ {
for k := 1; k <= i; k++ {
fmt.Printf("%d * %d = %d ", k, i, i*k)
}
fmt.Println()
}
return true, nil
}
func main() {
success, err := nn(8)
if err != nil {
fmt.Println(err, "执行失败")
} else {
fmt.Println(success, "执行成功")
}
// 上面已经 panic,这行永远走不到
fmt.Println("其余逻辑")
}
运行后会看到 panic 栈,且没有「其余逻辑」。这就是和业务错误最大的差别。
defer 用法
defer 会把一次函数调用推迟到 当前函数即将返回之前 执行,常用于关文件、解锁、以及配合 recover 捕获 panic。
基本顺序:先写正常逻辑,defer 的内容最后跑。
package main
import "fmt"
func main() {
fmt.Println("开始")
defer fmt.Println("defer:收尾")
fmt.Println("业务逻辑")
}
输出:
开始
业务逻辑
defer:收尾
多个 defer 按 后进先出(LIFO) 执行——最后注册的最先跑:
package main
import "fmt"
func main() {
defer fmt.Println("第一")
defer fmt.Println("第二")
defer fmt.Println("第三")
fmt.Println("函数体")
}
输出:
函数体
第三
第二
第一
和错误处理相关的关键点:
recover必须写在defer里才有效;普通代码里调用recover()拿不到 panic。defer常用来保证「出错也要收尾」:关文件、释放锁、打印现场。- 参数在写
defer那一刻就算好(不是真正执行时才算)。更多细节见「Go 函数」一文的 defer 小节。
用 defer + recover 兜住 panic
加上 recover 后,进程不会崩溃,可以打印 panic 信息;但 panic 发生点之后的代码仍然不会执行。
package main
import "fmt"
func nn(a int) (bool, error) {
if a != 9 {
panic("请输入9")
}
for i := 1; i <= a; i++ {
for k := 1; k <= i; k++ {
fmt.Printf("%d * %d = %d ", k, i, i*k)
}
fmt.Println()
}
return true, nil
}
func main() {
defer func() {
if info := recover(); info != nil {
fmt.Println("捕获到程序错误:", info)
}
}()
success, err := nn(8) // 这里 panic
if err != nil {
fmt.Println(err, "执行失败")
} else {
fmt.Println(success, "执行成功")
}
// panic 之后:同函数里这些代码依然不走
fmt.Println("其余逻辑")
}
输出类似:
捕获到程序错误: 请输入9
没有「执行失败 / 执行成功」,也没有「其余逻辑」。recover 的作用是:拦住崩溃,不是从 panic 下一行接着跑。
怎么选
| 情况 | 建议 |
|---|---|
| 用户输错、查不到数据、权限不够 | 返回 error |
库函数约定用 error(如 os.Open) | 跟着返回 / 包装 error |
| 明显的编程错误、不可继续的状态 | 可以 panic |
| 服务入口想避免整个进程挂掉 | 边界处 defer + recover,打日志后返回错误响应 |
入门阶段优先练好 if err != nil;panic / recover 留给真正的异常路径,不要拿来代替普通业务校验。
作业
- 返回 error:写
divide(a, b int) (int, error),b == 0时返回错误,否则返回商。在main里分别用(10, 2)和(10, 0)调用并处理。 - 业务错误对比:改写文中的
nn,允许传入1~9任意整数打印对应乘法表;小于1或大于9时返回error。调用失败后打印一句「其余逻辑」,确认它仍会执行。 - defer 顺序:在
main里注册三个defer,打印A、B、C,再打印一句「函数体」,观察输出顺序是否为「函数体 → C → B → A」。 - panic 与 recover:写一个函数
mustPositive(n int),n <= 0时panic。在main里用defer+recover捕获,打印 panic 信息;并在调用后面再写一句fmt.Println("其余逻辑"),验证它不会执行。 - 综合练习:写
printTable(n int) error(业务错误版)和mustPrintTable(n int)(n != 9就panic)。在main里先演示返回error后后续代码仍运行;再用recover演示panic被兜住但后续代码不运行。