错误处理
约 3923 字大约 13 分钟
2025-04-27
概述
Go 语言通过明确的返回值来处理错误,而不是使用异常机制。这种设计哲学让错误处理成为代码的一等公民,鼓励开发者显式地处理每一个可能的错误情况。
与 Python、Java 等语言不同,Go 没有 try-catch-finally 这样的异常处理结构。Go 的 error 是一个内置接口类型,函数通过返回 error 值来表明是否发生了错误,调用方有责任检查并处理它。
1. error 接口
Go 中的 error 是一个预定义的内置接口:
type error interface {
Error() string
}任何实现了 Error() string 方法的类型都自动满足 error 接口。
基本使用
package main
import (
"fmt"
"os"
)
// 打开文件时可能返回 error
func main() {
file, err := os.Open("nonexistent.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
}判断错误是否为空
package main
import "fmt"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
func main() {
// Go 中常见的错误检查模式
if result, err := divide(10, 0); err != nil {
fmt.Println("计算失败:", err)
} else {
fmt.Println("结果:", result)
}
// 或者分离写法
result, err := divide(10, 2)
if err != nil {
fmt.Println("计算失败:", err)
return
}
fmt.Println("结果:", result)
}2. 创建错误
errors.New — 创建简单的错误
errors.New 是最基本的创建错误的方式,它接受一个字符串并返回一个 error 值。
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("资源未找到")
var ErrPermission = errors.New("权限不足")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
if id == 999 {
return "", ErrPermission
}
return fmt.Sprintf("用户_%d", id), nil
}
func main() {
_, err := findUser(-1)
if err != nil {
fmt.Println("错误:", err) // 输出: 错误: 资源未找到
}
// 可以使用 == 比较 sentinel 错误
_, err = findUser(999)
if err == ErrPermission {
fmt.Println("权限错误,请联系管理员")
}
}fmt.Errorf — 格式化错误信息
fmt.Errorf 可以创建带格式化信息的错误,比 errors.New 更灵活。
package main
import "fmt"
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("年龄不能为负数: %d", age)
}
if age > 150 {
return fmt.Errorf("年龄不合法: %d", age)
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println(err) // 输出: 年龄不能为负数: -5
}
err = validateAge(200)
if err != nil {
fmt.Println(err) // 输出: 年龄不合法: 200
}
}3. 错误包装 (Wrapping)
错误包装允许你在保留原始错误信息的同时添加上下文,形成错误链。这在多层调用时非常有用。
%w 动词 — 包装错误
从 Go 1.13 开始,fmt.Errorf 支持 %w 动词来包装错误。
package main
import (
"errors"
"fmt"
"os"
)
func readConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
// 包装原始错误,添加上下文
return fmt.Errorf("读取配置文件失败: %w", err)
}
fmt.Println("配置内容:", string(data))
return nil
}
func loadApp() error {
err := readConfig("/etc/app/config.yaml")
if err != nil {
// 再包装一层
return fmt.Errorf("应用初始化失败: %w", err)
}
return nil
}
func main() {
err := loadApp()
if err != nil {
fmt.Println(err)
// 输出类似:
// 应用初始化失败: 读取配置文件失败: open /etc/app/config.yaml: no such file or directory
}
}errors.Is — 检查错误链
errors.Is 沿着错误链检查是否包含目标错误,即使错误被多次包装也能正确识别。
package main
import (
"errors"
"fmt"
"os"
)
var ErrNotExist = errors.New("文件不存在")
func readFile(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", ErrNotExist)
}
return nil
}
func processData(path string) error {
if err := readFile(path); err != nil {
return fmt.Errorf("数据处理失败: %w", err)
}
return nil
}
func main() {
err := processData("data.txt")
if err != nil {
fmt.Println("完整错误链:", err)
// errors.Is 会遍历整个错误链查找目标
if errors.Is(err, ErrNotExist) {
fmt.Println("根本原因: 文件不存在")
}
// 也可以检查 os 标准库的错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("底层系统错误: 文件或目录不存在")
}
}
}errors.As — 提取特定类型的错误
errors.As 从错误链中提取出符合特定类型的错误,并赋值给目标变量。
package main
import (
"errors"
"fmt"
"net"
)
type TimeoutError struct {
Duration int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("操作超时: %d 秒", e.Duration)
}
func connect() error {
return fmt.Errorf("连接失败: %w", &TimeoutError{Duration: 30})
}
func main() {
err := connect()
var timeoutErr *TimeoutError
if errors.As(err, &timeoutErr) {
fmt.Printf("检测到超时错误,超时时间: %d 秒\n", timeoutErr.Duration)
}
// 也可以提取 net.Error
var netErr net.Error
if errors.As(err, &netErr) {
fmt.Println("这是一个网络错误:", netErr)
}
}多层包装与链式检查
package main
import (
"errors"
"fmt"
)
var ErrInvalidInput = errors.New("无效输入")
var ErrNotFound = errors.New("资源未找到")
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s 验证失败: %s", e.Field, e.Message)
}
func validate(data map[string]string) error {
if name, ok := data["name"]; !ok || name == "" {
return &ValidationError{Field: "name", Message: "不能为空"}
}
return nil
}
func saveData(data map[string]string) error {
if err := validate(data); err != nil {
return fmt.Errorf("保存数据失败: %w", err)
}
// 模拟数据库查找
return fmt.Errorf("保存数据失败: %w", ErrNotFound)
}
func main() {
err := saveData(map[string]string{})
if err != nil {
fmt.Println("错误:", err)
// 检查是否是 ValidationError 类型
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("验证错误 - 字段: %s, 详情: %s\n", valErr.Field, valErr.Message)
}
// 检查 sentinel 错误
if errors.Is(err, ErrNotFound) {
fmt.Println("根本原因是资源未找到")
}
}
}4. 自定义错误类型
通过实现 Error() string 方法,可以创建携带额外上下文的自定义错误类型。
基础自定义错误
package main
import "fmt"
// 自定义错误类型
type MyError struct {
Code int
Message string
}
// 实现 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("错误码 %d: %s", e.Code, e.Message)
}
func doSomething(flag bool) error {
if !flag {
return &MyError{
Code: 400,
Message: "参数错误: flag 必须为 true",
}
}
return nil
}
func main() {
err := doSomething(false)
if err != nil {
fmt.Println(err) // 输出: 错误码 400: 参数错误: flag 必须为 true
// 类型断言获取具体错误信息
if myErr, ok := err.(*MyError); ok {
fmt.Printf("代码: %d, 消息: %s\n", myErr.Code, myErr.Message)
}
}
}丰富的自定义错误类型
package main
import (
"fmt"
"time"
)
// 详细的 HTTP 错误类型
type HTTPError struct {
StatusCode int
StatusText string
Body string
Timestamp time.Time
RequestID string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("[%s] HTTP %d %s: %s (req: %s)",
e.Timestamp.Format(time.RFC3339),
e.StatusCode,
e.StatusText,
e.Body,
e.RequestID,
)
}
// 可选:提供更多方法
func (e *HTTPError) IsClientError() bool {
return e.StatusCode >= 400 && e.StatusCode < 500
}
func (e *HTTPError) IsServerError() bool {
return e.StatusCode >= 500
}
func makeHTTPRequest(url string) error {
// 模拟 HTTP 请求失败
return &HTTPError{
StatusCode: 404,
StatusText: "Not Found",
Body: `{"error": "resource not found"}`,
Timestamp: time.Now(),
RequestID: "req-abc-123",
}
}
func main() {
err := makeHTTPRequest("https://api.example.com/users/999")
if err != nil {
fmt.Println("错误详情:", err)
if httpErr, ok := err.(*HTTPError); ok {
fmt.Printf("状态码: %d\n", httpErr.StatusCode)
fmt.Printf("请求 ID: %s\n", httpErr.RequestID)
fmt.Printf("客户端错误: %v\n", httpErr.IsClientError())
}
}
}5. panic
panic 用于表示真正异常且不可恢复的情况。当程序遇到无法继续运行的问题时,会触发 panic。
触发 panic 的场景
package main
import "fmt"
func main() {
// 1. 数组越界 — 自动触发 panic
// arr := []int{1, 2, 3}
// _ = arr[10] // panic: runtime error: index out of range [10] with length 3
// 2. 空指针解引用 — 自动触发 panic
// var p *int
// fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
// 3. 手动触发 panic
fmt.Println("程序开始")
// panic("发生了严重错误!") // 取消注释触发
fmt.Println("程序结束")
}手动触发 panic
package main
import "fmt"
func connectDB(dsn string) {
if dsn == "" {
panic("数据库连接字符串不能为空")
}
fmt.Println("数据库连接成功")
}
func main() {
fmt.Println("启动应用...")
// 使用 defer 确保某些清理操作
defer fmt.Println("应用关闭")
// connectDB("") // 取消注释会触发 panic
fmt.Println("应用运行中...")
}panic 的执行流程
当 panic 发生时,函数会立即停止执行,但会执行所有已注册的 defer 语句,然后 panic 沿着调用栈向上传播。
package main
import "fmt"
func a() {
defer fmt.Println("a() 的 defer")
fmt.Println("a() 开始")
b()
fmt.Println("a() 结束") // 不会执行
}
func b() {
defer fmt.Println("b() 的 defer")
fmt.Println("b() 开始")
panic("b 中发生了 panic")
// fmt.Println("b() 结束") // 不会执行
}
func main() {
defer fmt.Println("main() 的 defer")
a()
}输出:
a() 开始
b() 开始
b() 的 defer
a() 的 defer
main() 的 defer
panic: b 中发生了 panic注意:defer 语句在 panic 时仍然会按栈顺序执行。
6. recover
recover 用于从 panic 中恢复,它只能在 defer 函数中有意义地工作。
基本 recover 用法
package main
import "fmt"
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("从 panic 中恢复:", r)
}
}()
fmt.Println("执行可能 panic 的操作...")
panic("出错了!")
// fmt.Println("这行不会执行")
}
func main() {
safeFunction()
fmt.Println("程序继续执行...") // 这行会正常执行
}recover 的限制
package main
import "fmt"
// 错误用法 1: recover 不在 defer 中 — 无效
func wrongRecover1() {
// recover() // 无效,panic 发生时这里不会执行
panic("错误1")
}
// 错误用法 2: recover 在 defer 的嵌套函数中 — 无效
func wrongRecover2() {
defer func() {
// 这里调用了一个内嵌函数
func() {
if r := recover(); r != nil {
fmt.Println("这个 recover 不会生效")
}
}()
}()
panic("错误2")
}
// 错误用法 3: recover 不能跨 goroutine
func wrongRecover3() {
defer func() {
if r := recover(); r != nil {
fmt.Println("这个 recover 无法恢复另一个 goroutine 的 panic")
}
}()
go func() {
panic("另一个 goroutine 的 panic") // 主 goroutine 无法 recover
}()
}
// 正确用法
func correctRecover() {
defer func() {
if r := recover(); r != nil {
fmt.Println("成功恢复:", r)
}
}()
panic("出错了")
}
func main() {
fmt.Println("=== 正确用法 ===")
correctRecover()
fmt.Println("程序正常结束")
}带错误返回的 recover 模式
package main
import (
"fmt"
)
// 将 panic 转换为 error 返回
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("发生了 panic: %v", r)
}
}()
// 如果 b 为 0,这里会触发 panic(但通常应该用 error 而不是 panic)
result = a / b
return result, nil
}
func main() {
// 正常情况
result, err := safeDivide(10, 2)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
// 触发 panic 并被 recover
result, err = safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 输出: 错误: 发生了 panic: runtime error: integer divide by zero
} else {
fmt.Println("结果:", result)
}
fmt.Println("程序正常结束")
}7. panic/recover 完整示例
一个完整的示例:模拟 JSON 解析过程,将内部的 panic 转换为 error 返回。
package main
import (
"errors"
"fmt"
)
// ParseError 自定义解析错误
type ParseError struct {
Line int
Column int
Message string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("解析错误 (行:%d, 列:%d): %s", e.Line, e.Column, e.Message)
}
// JSONParser 简单的 JSON 解析器模拟
type JSONParser struct {
input string
pos int
}
// ParseJSON 安全的 JSON 解析入口
func ParseJSON(input string) (result interface{}, err error) {
parser := &JSONParser{input: input, pos: 0}
// 使用 defer + recover 捕获解析过程中的 panic
defer func() {
if r := recover(); r != nil {
// 判断 recover 到的类型
switch e := r.(type) {
case *ParseError:
err = e
case string:
err = fmt.Errorf("解析失败: %s", e)
default:
err = fmt.Errorf("解析失败: %v", e)
}
}
}()
// 在复杂的解析器中,可能会因为各种原因触发 panic
return parser.parse(), nil
}
// parse 模拟解析过程
func (p *JSONParser) parse() interface{} {
if len(p.input) == 0 {
panic(&ParseError{Line: 1, Column: 1, Message: "意外的文件结尾"})
}
// 模拟解析到无效字符
for i, ch := range p.input {
if ch == '{' {
continue
}
if ch == '}' {
return map[string]interface{}{}
}
if ch == '\x00' {
panic(&ParseError{Line: 1, Column: i + 1, Message: "空字符,可能不是有效的 JSON"})
}
}
panic("未知的解析错误")
}
// MustParseJSON 如果解析失败则 panic(适用于初始化时确定可以解析的情况)
func MustParseJSON(input string) interface{} {
result, err := ParseJSON(input)
if err != nil {
panic(err)
}
return result
}
func main() {
// 测试正常 JSON
result, err := ParseJSON(`{}`)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Printf("解析成功: %v\n", result)
}
// 测试空输入
_, err = ParseJSON("")
if err != nil {
fmt.Println("错误:", err)
}
// 测试无效输入
_, err = ParseJSON("hello\x00world")
if err != nil {
fmt.Println("错误:", err)
}
// 测试 MustParseJSON — 会在解析失败时 panic
fmt.Println("\n--- MustParseJSON 测试 ---")
// MustParseJSON("") // 取消注释会触发 panic
// 安全的 MustParseJSON 调用
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("MustParseJSON 触发了 panic:", r)
}
}()
MustParseJSON("")
}()
fmt.Println("程序正常结束")
}典型的 defer + recover + error 模式
在大型项目中,这种模式常用于将第三方库或复杂操作中的 panic 安全地转换为 error:
package main
import (
"database/sql"
"fmt"
)
// SafeDBQuery 安全的数据库查询包装
func SafeDBQuery(db *sql.DB, query string) (result string, err error) {
defer func() {
if r := recover(); r != nil {
switch r := r.(type) {
case error:
// 包装原始错误
err = fmt.Errorf("数据库查询 panic: %w", r)
default:
err = fmt.Errorf("数据库查询 panic: %v", r)
}
}
}()
// 这里调用可能触发 panic 的代码
// 例如:空指针、类型断言失败等
// _ = db.Query(query)
return "查询结果", nil
}
func main() {
var db *sql.DB // nil,会触发 panic
_, err := SafeDBQuery(db, "SELECT * FROM users")
if err != nil {
fmt.Println("安全的错误信息:", err)
}
}8. 最佳实践对比
errors vs panic 对比表
| 维度 | errors | panic |
|---|---|---|
| 适用场景 | 预期的、可恢复的错误 | 编程 bug、不可恢复的状态 |
| 常见例子 | 文件不存在、网络超时、输入校验失败 | 空指针解引用、数组越界、断言失败 |
| 控制流 | 常规返回值,调用方自主决定如何处理 | 异常中断,沿调用栈传播 |
| 性能开销 | 几乎为零 | 较大(栈展开 + defer 执行) |
| 可测试性 | 容易测试,只需检查返回值 | 难测试,需要 defer/recover 捕获 |
| 使用频率 | 90%+ 的错误场景 | 极少数场景 |
| 文档明确性 | 函数签名中明确返回 error | 隐式的,调用方不一定知道 |
推荐的做法
使用 error(推荐):
package main
import (
"errors"
"fmt"
)
// ✅ 好的做法:使用 error 表示预期错误
var ErrInsufficientFunds = errors.New("余额不足")
func withdraw(balance, amount float64) (float64, error) {
if amount <= 0 {
return balance, errors.New("取款金额必须大于 0")
}
if amount > balance {
return balance, ErrInsufficientFunds
}
return balance - amount, nil
}
func main() {
newBalance, err := withdraw(100, 200)
if err != nil {
if errors.Is(err, ErrInsufficientFunds) {
fmt.Println("请充值后再试")
} else {
fmt.Println("取款失败:", err)
}
return
}
fmt.Println("取款成功,余额:", newBalance)
}使用 panic(极少数情况):
package main
import "fmt"
// ✅ 合理的 panic 使用场景:程序初始化时的必要条件检查
func init() {
// 应用启动时检查关键依赖,失败则让程序崩溃
// 这样可以尽早发现问题
}
// ✅ 合理的 panic 使用场景:不应该发生的情况
func assert(condition bool, message string) {
if !condition {
panic("断言失败: " + message)
}
}
// ✅ 合理的 panic 使用场景:未实现的代码路径
type Animal interface {
Speak() string
}
type Dog struct{}
func (d *Dog) Speak() string {
return "汪汪"
}
type Cat struct{}
func (c *Cat) Speak() string {
return "喵喵"
}
type UnknownAnimal struct {
Type string
}
// 对于理论上不会出现的分支使用 panic
func (u *UnknownAnimal) Speak() string {
panic(fmt.Sprintf("未知动物类型: %s,需要在代码中添加该类型的处理", u.Type))
}
func animalSound(a Animal) {
fmt.Println("动物叫:", a.Speak())
}
func main() {
animalSound(&Dog{})
// 取消注释会导致 panic,提醒开发者需要实现该类型
// animalSound(&UnknownAnimal{Type: "外星生物"})
}常见的错误处理反模式
package main
import (
"fmt"
"os"
)
// ❌ 反模式 1: 忽略错误
func badReadFile(path string) {
data, _ := os.ReadFile(path) // 忽略了错误!
fmt.Println(string(data))
}
// ✅ 正确做法
func goodReadFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
fmt.Println("读取文件失败:", err)
return
}
fmt.Println(string(data))
}
// ❌ 反模式 2: 使用 panic 代替普通错误
func divideBad(a, b int) int {
if b == 0 {
panic("除数不能为 0") // 不应该用 panic!
}
return a / b
}
// ✅ 正确做法
func divideGood(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
// ❌ 反模式 3: 吞掉错误信息
func processBad() error {
_, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("处理失败") // 丢失了原始错误!
}
return nil
}
// ✅ 正确做法:包装原始错误
func processGood() error {
_, err := os.Open("file.txt")
if err != nil {
return fmt.Errorf("处理失败: %w", err) // 保留原始错误
}
return nil
}
func main() {
// 演示正确用法
result, err := divideGood(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
fmt.Println("程序继续执行")
}总结
- error 是一个接口 — 任何实现了
Error() string方法的类型都满足error接口 - 使用
errors.New和fmt.Errorf创建错误 - 错误包装 (
%w) 可以构建错误链,保留上下文信息 errors.Is遍历错误链检查 sentinel 错误errors.As从错误链中提取特定类型的错误- 自定义错误类型 可以携带额外的上下文信息
panic用于不可恢复的情况 — 程序 bug、初始化失败等recover只能在defer函数中生效,用于从 panic 中恢复- 优先使用 error,只在极端情况下使用 panic
- defer + recover + error 模式 可以将 panic 安全地转换为 error 返回
