Go 错误处理:error 接口与 if err != nil
Go 语言没有异常机制,而是通过内置的 error 类型来显式处理错误。理解 error 接口和 if err != nil 模式是写出健壮 Go 程序的基础。
1. 认识 error 接口
定义一个变量为 error 类型,本质上就是实现了如下接口:
type error interface {
Error() string
}
这意味着:只要一个类型有一个返回字符串的 Error() 方法,它就自动满足 error 接口。
创建自定义错误最简单的方式是使用标准库中的 errors.New:
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("文件未找到")
fmt.Println(err.Error()) // 输出:文件未找到
}
你也可以用 fmt.Errorf 动态生成带格式的错误信息:
err := fmt.Errorf("无法打开 %s: 权限不足", filename)
2. 函数如何返回错误
在 Go 中,函数通常将 error 作为最后一个返回值。调用者必须显式检查该值。
编写一个可能失败的函数:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
调用时必须处理错误:
result, err := divide(10, 0)
if err != nil {
fmt.Println("出错了:", err)
return
}
fmt.Println("结果:", result)
关键规则:当操作成功时,error 返回 nil;失败时返回非 nil 的 error 实例。
3. 正确使用 if err != nil
这是 Go 中最核心的错误处理模式。每次调用可能返回错误的函数后,立即检查 err 是否为 nil。
基本模式
value, err := someFunction()
if err != nil {
// 处理错误:记录、返回、退出等
log.Printf("操作失败: %v", err)
return err // 或 os.Exit(1),或默认值
}
// 继续使用 value
嵌套过深?提前返回!
避免“金字塔式”嵌套。一旦发现错误,立即返回,保持代码扁平:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件失败: %w", err)
}
// 处理 data...
return nil
}
注意这里使用了 %w 动词(Go 1.13+),它允许包装错误以便后续解包。
4. 错误比较与类型断言
有时你需要区分不同类型的错误以采取不同措施。
使用 errors.Is 判断特定错误
标准库中一些包定义了“哨兵错误”(sentinel errors),如 os.ErrNotExist。
不要直接用 == 比较(因为包装后可能不相等),而应使用 errors.Is:
_, err := os.Open("missing.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件确实不存在")
} else if err != nil {
fmt.Println("其他错误:", err)
}
使用 errors.As 提取错误类型
如果错误是一个结构体,你想访问其字段,使用 errors.As:
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Printf("路径错误: %s (操作: %s)", pathError.Path, pathError.Op)
}
这会沿着错误链查找是否包含 *os.PathError 类型。
5. 自定义错误类型
当需要携带额外信息(如错误码、上下文)时,定义自己的错误结构体。
创建一个带状态码的错误:
type APIError struct {
Code int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API错误 [%d]: %s", e.Code, e.Message)
}
返回它:
func callAPI() error {
return &APIError{Code: 404, Message: "资源未找到"}
}
使用 errors.As 获取原始类型:
err := callAPI()
var apiErr *APIError
if errors.As(err, &apiErr) {
if apiErr.Code == 404 {
// 重试或跳过
}
}
6. panic 不是错误处理
避免在常规错误处理中使用 panic。panic 应仅用于程序无法继续运行的致命错误(如配置解析失败、不可恢复的状态损坏)。
正确做法:始终通过 error 返回值传递可预期的失败。
例如,不要这样写:
// ❌ 错误示范
func badDivide(a, b float64) float64 {
if b == 0 {
panic("除零错误") // 不要这样做!
}
return a / b
}
而应返回 error,让调用者决定如何应对。
7. 最佳实践总结
| 场景 | 推荐做法 |
|---|---|
| 普通错误 | 立即检查 if err != nil,处理或返回 |
| 需要附加信息 | 使用 fmt.Errorf + %w 包装错误 |
| 区分错误类型 | 使用 errors.Is 或 errors.As |
| 自定义错误 | 实现 Error() string 方法 |
| 资源清理 | 在检查错误前使用 defer |
记住:Go 的错误处理哲学是“显式优于隐式”。虽然写起来略显啰嗦,但它迫使开发者直面失败路径,从而构建更可靠的系统。

暂无评论,快来抢沙发吧!