Go 的错误处理与 defer
Go 通常不会通过异常传递可预期的失败,而是把错误作为函数的返回值交给调用方处理。这样一来,正常结果和失败原因都会明确出现在函数签名与调用位置。
本文从多返回值开始,依次介绍 error、错误包装、panic、recover 和 defer,并说明如何使用 defer 安全地清理资源。
多返回值
Go 函数可以返回多个值。下面的函数同时返回除法结果和余数:
func divide(dividend, divisor int) (int, int) {
quotient := dividend / divisor
remainder := dividend % divisor
return quotient, remainder
}
func main() {
quotient, remainder := divide(10, 3)
fmt.Println(quotient, remainder) // 3 1
}如果不需要其中某个返回值,可以使用 _ 忽略它:
quotient, _ := divide(10, 3)多个返回值也适合同时表达计算结果和失败原因。按照 Go 的惯例,error 通常放在最后一个返回位置:
func divide(dividend, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("divisor cannot be zero")
}
return dividend / divisor, nil
}成功时返回计算结果和 nil,失败时返回结果类型的零值与具体错误。调用方应先判断 err,再使用结果:
result, err := divide(10, 0)
if err != nil {
fmt.Println("divide failed:", err)
return
}
fmt.Println(result)error 是一个值
error 是 Go 的内置接口,它只要求实现一个返回错误信息的方法:
type error interface {
Error() string
}因此,错误可以像其他值一样被创建、返回和判断。errors.New 适合创建内容固定的简单错误:
var ErrNonPositive = errors.New("number must be positive")把需要识别的错误声明为包级变量后,产生错误的函数和处理错误的调用方可以共享同一个值。这类值通常称为哨兵错误:
func requirePositive(number int) error {
if number <= 0 {
return ErrNonPositive
}
return nil
}错误信息应说明发生了什么,而错误处理则由调用方决定。函数不必在每一层都打印错误,否则同一个错误可能被重复记录多次。
包装错误
函数向上返回错误时,通常需要补充当前操作的上下文。fmt.Errorf 配合 %w 可以包装原始错误:
func parsePositive(input string) (int, error) {
number, err := strconv.Atoi(input)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", input, err)
}
if err := requirePositive(number); err != nil {
return 0, fmt.Errorf("validate %q: %w", input, err)
}
return number, nil
}这里的错误既保留了底层原因,也增加了“解析”或“验证”这层上下文。调用方可以直接展示完整错误信息:
validate "-2": number must be positive使用 %v 只能把错误信息写进新字符串,使用 %w 才会保留可供检查的错误链。
使用 errors.Is 判断错误
错误经过包装后,最外层的错误值已经不再等于原始错误。需要判断错误链中是否包含某个目标错误时,应使用 errors.Is:
number, err := parsePositive("-2")
if err != nil {
if errors.Is(err, ErrNonPositive) {
fmt.Println("please enter a positive number")
return
}
fmt.Println("invalid number:", err)
return
}
fmt.Println(number)errors.Is 会沿着由 %w 建立的错误链逐层检查,因此调用方不需要解析错误文本。
TIP
如果调用方需要读取自定义错误中携带的字段,可以定义错误类型并使用 errors.As。只需识别某个固定错误时,包级哨兵错误与 errors.Is 通常已经足够。
panic 与 recover 的边界
普通的输入错误、文件不存在或网络请求失败都属于可以预期的情况,应通过 error 返回。panic 表示当前函数无法继续维持程序所依赖的约束:
func mustPositive(number int) int {
if number <= 0 {
panic("number must be positive")
}
return number
}这类 must 函数只适合处理程序内部已经确保有效的数据。如果数字来自用户输入,函数应返回 error,而不是触发 panic。
发生 panic 后,当前函数会停止正常执行,并逐层退出调用栈。退出期间,已经登记的 defer 仍会执行。recover 只能在延迟调用的函数中捕获当前 Goroutine 正在传播的 panic:
func runSafely(task func()) {
defer func() {
if value := recover(); value != nil {
fmt.Println("task panicked:", value)
}
}()
task()
}recover 适合放在程序入口、任务执行器等明确的边界,避免一个任务的 panic 直接终止整个进程。捕获后仍应记录必要信息,并决定当前任务能否安全结束。
WARNING
recover 不能捕获另一个 Goroutine 中的 panic,也不应被当作普通错误处理方式。过早恢复可能让程序在状态不完整的情况下继续运行。
defer 的执行时机
defer 用于登记一个稍后执行的函数调用。包含它的外层函数即将返回时,延迟调用才会执行:
func example() {
defer fmt.Println("cleanup")
fmt.Println("working")
}输出顺序如下:
working
cleanup无论函数执行到末尾,还是提前遇到 return,已经登记的延迟调用都会在函数返回前执行。发生 panic 时,它们也会在调用栈退出过程中执行。
参数会立即求值
执行到 defer 语句时,延迟函数的参数会立刻求值:
func example() {
message := "before"
defer fmt.Println(message)
message = "after"
fmt.Println(message)
}输出结果是:
after
before如果需要在函数返回时读取变量的最新值,可以延迟执行一个匿名函数:
func example() {
message := "before"
defer func() {
fmt.Println(message)
}()
message = "after"
}匿名函数的函数体在最后才执行,因此它会读取到 "after"。
多个 defer 后进先出
同一个函数中的多个 defer 按照登记顺序的相反方向执行:
func example() {
defer fmt.Println("first")
defer fmt.Println("second")
fmt.Println("body")
}输出结果是:
body
second
first后获得的资源通常要先释放,这种后进先出的顺序正好符合逐层清理资源的需要。
使用 defer 清理资源
成功获得资源后,可以立即使用 defer 登记清理操作。下面的函数打开文件并读取内容:
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file: %w", err)
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
return content, nil
}defer file.Close() 必须放在错误检查之后。只有 os.Open 成功时,file 才是可以关闭的有效资源。完成登记后,即使读取失败并提前返回,文件也会被关闭。
只读文件通常可以采用上面的写法。如果写入文件时 Flush 或 Close 的失败会影响数据完整性,就应显式检查清理操作返回的错误。
defer 的作用域是整个函数,而不是当前代码块。在一个长循环中反复登记资源清理,资源会一直保留到外层函数返回。可以把单轮工作提取成独立函数,让每轮结束时及时执行清理:
func printFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}
func printFiles(paths []string) error {
for _, path := range paths {
if err := printFile(path); err != nil {
return fmt.Errorf("print %q: %w", path, err)
}
}
return nil
}总结
Go 使用多返回值把正常结果和失败原因一起交给调用方。简单错误可以通过 errors.New 创建,向上返回时使用 %w 补充上下文,再通过 errors.Is 检查错误链。
panic 和 recover 只用于无法继续维持程序约束的情况及明确的恢复边界。对于普通失败,返回 error 仍然是首选。defer 则适合把清理操作放在资源获取位置附近,但要注意参数立即求值、多个调用后进先出,以及它绑定整个函数的执行范围。