错误处理机制
什么是错误?
在 Go 语言中,错误(error)是一种内置的类型,用于表示程序中的错误状态。Go 的错误处理方式与其他编程语言有所不同,它采用显式的错误处理机制。
异常处理对比
在 Java
和 C#
等编程语言中,错误处理通常是通过 try-catch
机制来管理的。当程序在 try
块中遇到错误时,catch
块会捕获该错误,并执行相应的处理逻辑。这种机制为处理异常提供了一种结构化的方法,确保即使在发生错误的情况下,应用程序也不会意外崩溃。
与此不同,Go
语言采用了一种完全不同的错误处理方式。在 Go
中,没有传统意义上的异常处理机制。相反,Go
将错误视为函数的返回值之一。这意味着在调用函数后,开发者需要主动检查是否返回了错误,并根据情况决定如何处理它。这种方法更加强调显式的错误处理,而不是像 try-catch
那样隐式的异常处理。这不仅使代码逻辑更为清晰,还鼓励了更好的错误管理实践。
演示错误
尝试打开一个不存在的文件:
package main
import (
"fmt"
"os"
)
func main() {
// 使用 os 包的 Open 函数打开文件
file, err := os.Open("README.md")
if err != nil {
fmt.Println(err)
return
}
// 打印文件名称和打印文件打开成功
fmt.Println(file.Name(), "open file success.")
}
在 os
包中有打开文件的功能函数:func Open(name string) (*File, error) 如果文件已经成功打开,那么 Open 函数将返回文件处理。如果在打开文件时出现错误,将返回一个非 nil 错误。
如果一个函数或方法返回一个错误,那么按照惯例,它必须是函数返回的最后一个值。因此,Open 函数返回的值是最后一个值。
处理错误的惯用方法是将返回的错误与nil进行比较。nil 值表示没有发生错误,而非 nil 值表示出现错误。在我们的例子中,我们检查错误是否为 nil。如果它不是 nil,我们只需打印错误并从主函数返回。
运行结果:
open README.md: no such file or directory
程序输出结果是得到了一个错误,说明该文件不存在。
error类型
在 Go语言标准库的内置包 builtin
的 error
接口来表示错误,源码如下:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
任何实现了 Error() string
方法的类型都满足这个接口。
既然我们支持错误是一种接口,可以在源码中了解更多关于错误的信息。
在上述演示错误案例中,我们仅仅是打印了错误的描述。如果我们想要的是导致错误的文件的实际路径。一种可能的方法是解析错误字符串。这是我们程序的输出:
open README.md: no such file or directory
我们可以解析这个错误消息并从中获取文件路径 "README.md"。但这是一个糟糕的方法。在新版本的语言中,错误描述可能随时更改,我们的代码将会中断。
是否有办法可靠地获取文件名?答案是肯定的,它可以做到,Go 标准库使用不同的方式提供更多关于错误的信息。
断言底层结构类型并从结构字段获取更多信息
如果仔细阅读 Open()
函数的文档,可以看到它返回的是 PathError
类型的错误。
PathError
是一个结构体,标准库的实现代码如下:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
从上面的代码中,我可以理解 PathError
通过声明 Error() string
方法实现了错误接口。该方法连接操作、路径和实际错误并返回它。这样我们就得到了错误信息:
open README.md: no such file or directory
PathError
结构体的路径字段包含导致错误的文件的路径。让我们修改上面写的程序,并打印出路径,修改后的代码如下:
package main
import (
"fmt"
"os"
)
func main() {
// 使用 os 包的 Open 函数打开文件
file, err := os.Open("README.md")
if err, ok := err.(*os.PathError); ok {
fmt.Println("File at path", err.Path, "failed to open")
return
}
// 打印文件名称和打印文件打开成功
fmt.Println(file.Name(), "open file success.")
}
在上面的程序中,我们使用类型断言获得错误接口的基本值。然后自定义错误输出:
File at path README.md failed to open
断言底层数据结构并使用方法获取更多信息
获得更多信息的第二种方法是断言底层类型,并通过调用 struct
类型的方法获取更多信息。
如下 DNSError:
// DNSError represents a DNS lookup error.
type DNSError struct {
// ...
}
// Unwrap
func (e *DNSError) Unwrap() error {
// ...
}
// Error
func (e *DNSError) Error() string {
// ...
}
// Timeout
func (e *DNSError) Timeout() bool {
// ...
}
// Temporary
func (e *DNSError) Temporary() bool {
// ...
}
从上面的代码中可以看到,DNSErrorstruct 有三个方法,Timeout() bool
和 Temporary() bool
它们返回一个布尔值,表示错误是由于超时还是临时的,Unwrap() error
返回一个 error 类型的值,这个方法用于返回嵌套的错误。如果 DNSError 中包含另一个错误(例如,底层 DNS 查找引发的错误),这个方法将返回那个错误。这个方法的主要目的是实现 Go 1.13 引入的错误处理机制,允许错误链的提取。
让我们编写一个断言 DNSError 类型的程序,并调用这些方法来确定错误是临时的还是超时的。
代码如下:
package main
import (
"fmt"
"net"
)
func main() {
addr, err := net.LookupHost("google.com.cn")
// 断言错误类型
if err, ok := err.(*net.DNSError); ok {
if err.Timeout() {
fmt.Println("operation timed out")
} else if err.Temporary() {
fmt.Println("temporary error")
} else {
fmt.Println("generic error: ", err)
}
return
}
fmt.Println(addr)
}
在上面的程序中,我们正在尝试获取一个无效域名的 IP 地址,这是一个无效的域名:google.com.cn
在我们的例子中,错误既不是暂时的,也不是由于超时,因此程序会打印出来:
generic error: lookup google.com.cn: no such host
创建error
创建一个error有以下两种方式,第一种是使用 errors
包下的 New()
函数,第二种是使用 fmt
包下的 Errorf()
函数,可以得到一个格式化参数的error。
下面是完整的示例:
package main
import (
"errors"
"fmt"
)
func main() {
// 第一种方式创建 error 通过 errors 包的 New() 函数
// res1, err := sum1(10, 20)
res1, err := sum1(-10, -20)
if err != nil {
panic(err)
}
fmt.Println(res1)
}
// sum1
func sum1(a, b int) (int, error) {
if a < 0 && b < 0 {
return 0, errors.New("参数必须大于0")
}
return a + b, nil
}
package main
import "fmt"
func main() {
// 第二种方式创建 error 通过 fmt 包的 Errorf() 函数
// res2, err := sum2(11, 22)
res2, err := sum2(-11, -22)
if err != nil {
panic(err)
}
fmt.Println(res2)
}
// sum2
func sum2(a, b int) (int, error) {
if a < 0 && b < 0 {
return 0, fmt.Errorf("参数必须大于0")
}
return a + b, nil
}
大部分情况,为了更好的维护性,一般都不会临时创建error,而是会将常用的error当作全局变量使用,例如下方节选自 os\erros.go
文件的代码。
// Portable analogs of some common system call errors.
//
// Errors returned from this package may be tested against these errors
// with [errors.Is].
var (
// ErrInvalid indicates an invalid argument.
// Methods on File will return this error when the receiver is nil.
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)
自定义error
在 Go 语言中,可以通过实现 error
接口中的 Error()
方法来自定义错误。例如 erros
包下的 errorString
就是一个很简单的实现。
func New(text string) error {
return &errorString{text}
}
// errorString结构体
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
因为 errorString
实现太过于简单,表达能力不足,所以很多开源库包括官方库都会选择自定义error,以满足不同的错误需求。
以下是一个自定义错误的示例:
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("Code: %d, Message: %s", e.Code, e.Message)
}
func NewMyError(code int, message string) error {
return &MyError{
Code: code,
Message: message,
}
}
通过这种方式,我们可以定义更丰富的错误信息。
错误传递
在函数调用中传递错误是Go语言处理错误的常见方式。通过返回值返回错误,并在调用方进行检查:
func doSomething() error {
// 一些操作
if someCondition {
return errors.New("发生了错误")
}
return nil
}
func main() {
err := doSomething()
if err != nil {
fmt.Println("Error:", err)
}
}
错误链
在 go1.13 引入了 errors
包中的 Unwrap()
、Is()
和 As()
函数用于错误链的处理。可以在创建错误时使用fmt.Errorf来包装错误:
err := fmt.Errorf("additional context: %w", originalErr)
不过,如果我们使用了占位符 %w
时,将不会使用上述方法,而是使用了 wrapError:
type wrapError struct {
msg string
err error
}
func(e *wrapError) Error()string{
return e.msg
}
func(e *wrapError) Unwrap() error{
return e.err
}
使用这种方式主要是为了错误链,那就让我们看一下如何使用错误链的相关操作。
要是想判断封装的这么多层的错误中,有没有哪一层错误等于我们要的值,可以使用 Is()
这个函数进行判断:
package main
import (
"errors"
"fmt"
)
// 定义一个自定义错误类型
var ErrNotFound = errors.New("item not found")
func findItem(id int) error {
// 模拟查找失败的情况
return fmt.Errorf("findItem: %w", ErrNotFound) // 使用 %w 包装错误
}
func main() {
err := findItem(42)
if err != nil {
// 使用 errors.Is 检查错误
if errors.Is(err, ErrNotFound) {
fmt.Println("The item was not found.")
} else {
fmt.Println("An unexpected error occurred:", err)
}
}
}
然后就是 As()
函数,这个方法跟上文的 Is() 类似,只不过它判断的是类型是否一致。
package main
import (
"errors"
"fmt"
)
// 定义一个自定义错误类型
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.Message
}
// 一个函数返回 MyError 类型的错误
func doSomething() error {
return &MyError{"something went wrong"}
}
func main() {
err := doSomething()
if err != nil {
// 使用 errors.As 检查错误并将其转换为 *MyError 类型
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("Caught a MyError:", myErr.Message)
} else {
fmt.Println("An unexpected error occurred:", err)
}
}
}
接下来是 erros
包中的 Join()
函数的作用是将多个错误合并成一个错误。这对于处理多个错误非常有用,特别是在需要同时报告多个错误的情况下。
package main
import (
"errors"
"fmt"
)
func main() {
// 创建几个错误
err1 := errors.New("error 1")
err2 := errors.New("error 2")
err3 := errors.New("error 3")
// 使用 Join 函数将错误合并
combinedErr := errors.Join(err1, err2, err3)
// 打印合并后的错误
fmt.Println(combinedErr)
}
panic
panic
是 Go 语言提供的一种用于处理程序中不可恢复错误的机制。当 panic
被触发时,程序会立即停止当前函数的正常执行流程,开始沿着调用堆栈执行被延迟的(deferred)函数。如果 panic
没有被恢复(recover),程序将会崩溃并输出堆栈跟踪信息。
panic触发场景(运行时触发):
- 数组或切片越界访问
- 空指针引用
- 向已关闭的channel发送数据
- 类型断言失败
- 死锁
- 栈溢出
显式触发 panic 通过调用 panic() 函数手动触发,例如:
func doSomething() {
panic("something went terribly wrong")
}
panic
会沿着调用栈向上传播,触发后当前函数会立即停止执行。在传播过程中会执行当前函数中已经注册的 defer
语句,然后返回到调用者继续传播。
func example() {
defer fmt.Println("1")
defer fmt.Println("2")
panic("发生panic")
defer fmt.Println("3") // 这一行不会执行
}
// 输出:
// 2
// 1
// panic: 发生panic
适合使用panic的场景:
- 初始化程序时遇到无法恢复的错误
- 程序遇到严重的资源问题
- 检测到不可能发生的情况
不适合使用panic的场景:
- 可预期的错误情况(使用error返回值)
- 正常的业务逻辑控制
- 用户输入验证
示例代码:
func initDatabase() {
db, err := sql.Open("postgres", connectionString)
if err != nil {
panic(fmt.Sprintf("无法连接数据库: %v", err))
}
// ... 其他初始化代码
}
recover
recover
是 Go 语言提供的一种用于恢复 panic
的内建函数。它只能在 defer 函数中生效,用于捕获和处理 panic
,防止程序崩溃。
使用规则
- 只能在defer函数中调用
- 获取panic的值并恢复正常执行
- 如果没有panic发生,recover返回nil
- recover后程序可以继续执行
标准用法:
func someFunction() {
defer func() {
if err := recover(); err != nil {
// 处理panic
fmt.Printf("捕获到panic: %v\n", err)
}
}()
// 可能发生panic的代码
}
高级用法:
func recoverSelectively() {
defer func() {
if err := recover(); err != nil {
switch err.(type) {
case *CustomError:
// 处理自定义错误
fmt.Printf("处理自定义错误: %v\n", err)
default:
// 其他类型的panic,继续传播
panic(err)
}
}
}()
// ... 业务代码
}
在 goroutine 中使用:
func handleGoroutinePanic() {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("goroutine panic: %v\n", err)
}
}()
// 可能panic的代码
}()
}
实践中推荐用法:
- 在程序的顶层使用recover
- 记录详细的错误信息
- 恢复后确保程序状态一致性
- 虑是否需要重试操作
需要避免的用法:
- 在不必要的地方使用recover
- 滥用recover掩盖真正的问题
- 在循环中重复recover
- 忽略recover后的错误处理
完整的代码示例:
package main
import (
"fmt"
"log"
)
// 自定义错误类型
type BusinessError struct {
Code int
Message string
}
func (e *BusinessError) Error() string {
return fmt.Sprintf("业务错误 [%d]: %s", e.Code, e.Message)
}
// 包装recover的工具函数
func SafelyDo(work func()) {
defer func() {
if err := recover(); err != nil {
log.Printf("检测到panic: %v\n", err)
// 这里可以添加报警或监控代码
}
}()
work()
}
func main() {
// 示例1:处理自定义业务错误
SafelyDo(func() {
processBusinessLogic("")
})
// 示例2:处理运行时错误
SafelyDo(func() {
var slice []int
slice[0] = 1 // 将触发panic
})
fmt.Println("程序继续执行...")
}