Menu
CoddyTech

Panic、Defer 和 Recover

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 57 课,共 107 课。

虽然 Go 强调返回错误,但有些情况确实无法恢复。针对这些情况,Go 提供了 panic、defer 和 recover,这是一种处理特殊情况的机制。

panic 会立即停止正常执行。它专用于访问越界索引等编程错误,而不用于无效用户输入等预期会发生的错误:

func MustGetConfig(key string) string {
    value, exists := config[key]
    if !exists {
        panic("missing required config: " + key)
    }
    return value
}

defer 会安排一个函数在外层函数返回时运行,无论是正常返回还是因 panic 而返回。延迟调用会按相反的顺序执行(后进先出):

func ProcessFile() {
    fmt.Println("Opening file")
    defer fmt.Println("Closing file")
    fmt.Println("Processing...")
    // 输出:Opening file, Processing..., Closing file
}

recover 捕获 panic 并恢复正常执行。它只能在延迟函数中使用:

func SafeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    
    riskyOperation()
    return nil
}

这种模式可以将 panic 转换为常规错误,让程序继续运行。请谨慎使用:对于预期会发生的失败情况,优先返回错误;只有在状态损坏或无法恢复等真正特殊的情况下,才使用 panic/recover。

challenge icon

挑战

简单

让我们构建一个安全的除法计算器,演示如何使用 panic、defer 和 recover 优雅地处理异常情况。计算器会尝试执行有风险的操作,并将 panic 转换为可以正常处理的常规错误。

你需要将代码分成两个文件:

  • calculator.go:编写除法逻辑,并在发生 panic 时进行恢复。

    实现一个 Divide 函数,接收两个整数。如果除数为零,就使用消息 "division by zero" 触发 panic。否则,返回除法的整数结果。

    实现一个 SafeDivide 函数,接收两个整数,并同时返回一个整数结果和一个错误。这个函数应该:

    • 使用 defer 配合匿名函数,从任何 panic 中恢复
    • 如果恢复了 panic,就将其转换为错误,格式为 "calculation error: [panic message]"
    • 在内部调用 Divide 函数
    • 如果成功,就返回结果和 nil;如果恢复了 panic,则返回零和错误
  • main.go:从输入中读取两个整数,并使用 SafeDivide 安全地进行计算。还要通过按正确顺序打印清理消息来演示 defer 的用法。

    你的主函数应该:

    • 在开头打印 Starting calculation
    • 使用 defer 安排打印 Cleanup complete
    • 再使用一个 defer 安排打印 Releasing resources
    • 使用输入的值调用 SafeDivide
    • 根据结果打印 Result: [value] 或 Error: [error message]

输入内容如下:

  • 第 1 行:第一个整数(被除数)
  • 第 2 行:第二个整数(除数)

请记住,延迟调用会按相反的顺序执行(后进先出),因此清理消息的显示顺序应与安排它们的顺序相反。

例如,输入 20 和 4 时,输出应为:

Starting calculation
Result: 5
Releasing resources
Cleanup complete

输入 10 和 0 时,输出应为:

Starting calculation
Error: calculation error: division by zero
Releasing resources
Cleanup complete

亲手试试

package main

import (
	"fmt"
)

func main() {
	// 读取输入
	var dividend, divisor int
	fmt.Scanln(&dividend)
	fmt.Scanln(&divisor)

	// 打印开始消息
	fmt.Println("Starting calculation")

	// TODO:使用 defer 安排输出 "Cleanup complete" 消息

	// TODO:使用 defer 安排输出 "Releasing resources" 消息
	// 记住:延迟调用按 LIFO 顺序执行(后进先出)

	// TODO:用输入值调用 SafeDivide

	// TODO:打印 "Result: [value]" 或 "Error: [error message]"
	// 具体取决于是否返回错误
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器