Go 中 Panic、Recovery 和 Defer 语句的错误处理

在大多数情况下,代码中的正常错误很容易预料到,但恐慌属于意外错误的类别。无论如何,我们都可以推迟函数调用来执行恐慌,但是有更好的方法来处理它们,正如我们将在这个 Go 开发者教程中展示的那样。

恐慌会导致程序突然终止,因此开发人员应该使用恢复功能来控制它并在程序退出之前清理一些混乱。本文通过使用 Go 编程语言的示例来描述这些概念。

Go 中的错误处理新手?查看我们的配套文章:如何处理 Go 中的错误

在 Go 或 Golang 中处理Panics恐慌

代码中经常出现错误或错误。虽然真正写得好的代码可以声称错误更少,但事实是,即使是最好的代码也很少没有错误。此外,在添加功能、修改代码、重构和更新过程中,错误可能会蔓延到代码中。小错误会在程序执行期间产生故障,但可能并不总是使系统崩溃。然而,这并不意味着他们不那么严重。事实上,如果不根除,从长远来看,这些类型的问题可能会更加危险。

编写无错误的代码并不容易;一个好的程序实际上是不断编码和调试过程的结果。优秀的编码人员会意识到这一点并优雅地处理错误。可以预见的错误会立即由程序员处理。

Go 的类型系统可以在编译时检测多种类型的错误,如果发现错误,则会在编译期间标记错误消息。但是,有些错误是无法预料的,​​只能在运行时检测到。例如,来自一个函数或方法调用另一个无效输入的错误只能通过执行代码来检测。不同的编程语言使用不同的技术来处理这些类型的错误。例如,在C中,程序员负责验证每个输入并检查返回值。同时,Java可以使用异常来延迟错误处理。Go 没有单一的解决方案,而是根据错误发生的模式工作并在适当的时间处理它们。

如何在 Go 中使用 Defer 语句

在错误的发生和编写错误处理规定的地方之间总是存在差距。开发人员必须确保代码即使在这个中间的关键区域中也是安全的。错误的存在或不存在不应该能够破坏代码,并且代码应该执行,尽管可能发生任何错误。

C++中,这种情况可以由本地析构函数处理。在Java中,它由finally语句处理。在 Go 中,我们使用deferdefer语句中的函数调用发生在函数退出之后。这种机制可以有效地用于释放关键系统资源——例如关闭打开的文件——以确保我们的代码不会泄漏文件描述符。

defer 语句是一个以defer关键字为前缀的普通函数调用。尽管延迟函数参数表达式是在语句执行期间计算的,但实际调用会被延迟,直到包含 defer 语句的函数完成执行,无论是否有错误。如果有多于一份延期结单;它们以与它们被推迟的相反顺序执行。

通常,defer语句与资源的打开和关闭、连接和断开连接或锁定和解锁配对,以确保资源被优雅地释放,而不管发生错误。以下是Go中的defer示例:

package main

import (
	"fmt"
	"os"
)

func openOrCreateFile(fileName string) *os.File {
	file, err := os.Create(fileName)
	if err != nil {
		panic(err)
	} else {
		return file
	}
}

func closeFile(file *os.File) {
	err := file.Close()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error! %v\n", err)
		os.Exit(1)
	}
}

func writeToFile(file *os.File) {
	fmt.Fprintln(file, "This is a sample text.")
}

func main() {

	fileName := "sample.txt"
	file := openOrCreateFile(fileName)
	defer closeFile(file)
	writeToFile(file)
}

Panic in Go

Go 编程语言没有与其他语言相同的异常;但是,它确实提供了与恐慌Panic呼叫类似的想法(语义上)。Go 中的类型系统在编译过程中捕获了几个错误,但是只有在运行时才能检测到错误,例如访问数组越界或 nil 指针取消引用。当 Golang 遇到这样的错误时,它会恐慌

恐慌Panic导致程序的正常执行停止,但所有延迟的函数调用都在它崩溃之前执行,并显示日志消息。日志消息提供了恐慌Panic的原因,并包含了恐慌Panic值、错误消息、堆栈跟踪,简而言之,包括诊断问题原因所需的所有信息。在下面的示例 Go 代码中,我们将创建一个快速的越界恐慌

package main

import "fmt"

func main() {

	deck := []string{"Spade", "Club", "Heart", "Diamond"}
	fmt.Println("This will cause panic", deck[len(deck)])
}

当您在 IDE 或代码编辑器中运行上述代码时,会产生以下输出:

panic: runtime error: index out of range [4] with length 4
goroutine 1 [running]:
main.main()
        /home/mano/Documents/go_workspace/proj1/main.go:8 +0x1b

请注意,名称“索引超出范围”提供了错误提示。Go 编译器不知道函数len()返回的值或在代码执行之前返回的值。结果,编译器无法在编译期间捕获错误。如果代码中存在这样的错误,则运行时系统除了停止执行之外别无选择。恐慌消息为开发人员纠正问题提供了线索。

有趣的是,有一个内置的恐慌功能。这意味着我们可以拨打恐慌电话。可以直接调用内置的panic函数。它接受一个字符串值作为参数。这种灵活性提供给程序员,他们可以选择自己的一组恐慌并在自己的代码的斜坡上咆哮。这在创建我们自己的自定义包允许我们告诉用户他们犯了某些错误的情况下特别有用。简而言之,它让开发人员为自己的恐慌设定规则。

这是一个代码片段,展示了如何使用panic函数。请注意,我们在第一个示例中也使用了panic函数:

switch day := weekDay(chooseWeekDay()); s {
	case "Sunday":
	// ...
	case "Wednesday":
	// ...
	case "Friday": // ...
	case "Saturday":
	// ...
	default:
	panic(fmt.Sprintf("invalid weekday %q", s)) // panic!
}

panic函数的调用会导致程序崩溃,只能在严重的情况下使用。对每个错误都使用恐慌不是一个好主意。相反,尝试使用其他方法优雅地处理错误。话虽如此,在极端情况下使用恐慌是可以的。

从Go Panic中恢复

恐慌意味着错误获胜并且程序退出执行。但是,有时可以恢复,例如在崩溃前执行一些清理。例如,如果在数据库连接过程中发生意外情况,我们可能能够向客户端报告程序由于连接错误而突然退出,而不是闲逛。这仅仅意味着我们可以处理恐慌和错误。有一个名为recover的内置函数允许开发人员通过调用堆栈拦截panic并防止程序突然终止。

这是一个展示如何在 Go中使用恢复功能的示例:

package main

import (
	"fmt"
	"log"
)

func main() {
	divByZero()
	fmt.Println("Although panicked. We recovered. We call mul() func")
	fmt.Println("mul func result: ", mul(5, 10))
}

func div(x, y int) int {
	return x / y
}

func mul(x, y int) int {
	return x * y
}

func divByZero() {
	defer func() {
		if err := recover(); err != nil {
			log.Println("panic occurred:", err)
		}
	}()
	fmt.Println(div(1, 0))
}

上面的代码产生以下输出:

2021/08/24 19:13:55 panic occurred: runtime error: integer divide by zero
Although panicked. We recovered. We call mul() func
mul func result:  50

一个编写良好的程序在panic时退出。这是正常的反应。我们只能在某些特定情况下恢复。事实上,恐慌后的恢复并不总是一个好主意,必须谨慎使用。恐慌只有在出现问题时才会发生。尝试恢复已经错误的程序的执行并不是一个好主意。这可能会导致程序中出现意外行为。决定何时使用恐慌很容易,但很难决定何时恢复。Go 中存在恢复机制,但应极其谨慎地使用。

使用 Go Final Thoughts 处理错误

在这个 Go 教程中,我们解释了Golang 必须提供的deferpanicrecovery机制。它们是 Go 中错误处理过程的重要组成部分。defer语句是错误处理中最常见且经常使用的语句,而恢复很少使用且非常谨慎。在极少数情况下也需要显式调用恐慌函数。理解这三种错误处理机制对于在 Go 中编写生产就绪代码非常重要。