Go 语言错误处理全面指南

其实你心里早就有了答案,你就是不想承认,你就是不甘心爱了那么久的人,连个像样的交代都给不了你

Posted by yishuifengxiao on 2024-10-28

基础错误处理方法

返回错误值(最常用)

func ReadFile(filename string) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return nil, err
}

return data, nil
}

func main() {
data, err := ReadFile("config.yaml")
if err != nil {
log.Printf("文件读取失败: %v", err)
// 处理错误或返回
}
// 使用 data...
}

适用场景:

  • 大多数常规函数调用
  • I/O 操作、网络请求、数据库查询等可能失败的操作
  • 需要立即处理错误的场景

错误包装(Error Wrapping)

Go 1.13+ 引入了错误包装机制

func LoadConfig() (Config, error) {
data, err := ReadFile("config.yaml")
if err != nil {
return Config{}, fmt.Errorf("加载配置失败: %w", err)
}

var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("解析配置失败: %w", err)
}

return cfg, nil
}

func main() {
cfg, err := LoadConfig()
if err != nil {
// 检查错误链中的特定错误
if errors.Is(err, os.ErrNotExist) {
log.Println("配置文件不存在")
}
log.Printf("配置加载失败: %v", err)
}
}

适用场景:

  • 需要添加上下文信息到错误中
  • 保留原始错误信息供后续检查
  • 构建错误调用链以帮助调试

高级错误处理模式

自定义错误类型

type APIError struct {
StatusCode int
Message string
Err error
}

func (e *APIError) Error() string {
if e.Err != nil {
return fmt.Sprintf("API错误(%d): %s [%v]", e.StatusCode, e.Message, e.Err)
}
return fmt.Sprintf("API错误(%d): %s", e.StatusCode, e.Message)
}

func (e *APIError) Unwrap() error {
return e.Err
}

func CallAPI(url string) error {
resp, err := http.Get(url)
if err != nil {
return &APIError{
StatusCode: 0,
Message: "请求失败",
Err: err,
}
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
return &APIError{
StatusCode: resp.StatusCode,
Message: "API返回错误状态",
}
}

// 处理成功响应
return nil
}

func main() {
err := CallAPI("https://api.example.com/data")
if err != nil {
var apiErr *APIError
if errors.As(err, &apiErr) {
log.Printf("API错误: 状态码 %d, 消息: %s", apiErr.StatusCode, apiErr.Message)
if apiErr.StatusCode == 429 {
// 处理限流错误
}
} else {
log.Printf("其他错误: %v", err)
}
}
}

适用场景:

  • 需要携带额外错误信息(如状态码、错误代码等)
  • API 错误处理
  • 需要根据错误类型进行不同处理的场景

错误哨兵(Sentinel Errors)

var (
ErrUserNotFound = errors.New("用户不存在")
ErrInvalidRequest = errors.New("无效请求")
)

func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidRequest
}

user, exists := userDB[id]
if !exists {
return nil, ErrUserNotFound
}

return user, nil
}

func main() {
user, err := GetUser(42)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// 创建新用户
} else if errors.Is(err, ErrInvalidRequest) {
// 返回客户端错误
}
}
}

适用场景:

  • 定义可预期的、明确的错误条件
  • 公共库中的标准错误定义
  • 需要直接比较错误的场景

错误处理中间件

type HandlerFunc func(http.ResponseWriter, *http.Request) error

func ErrorMiddleware(next HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := next(w, r)
if err != nil {
switch e := err.(type) {
case *APIError:
w.WriteHeader(e.StatusCode)
json.NewEncoder(w).Encode(map[string]string{"error": e.Message})
case ValidationError:
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(e.Errors)
default:
w.WriteHeader(http.StatusInternalServerError)
log.Printf("内部错误: %v", err)
}
}
}
}

func main() {
http.Handle("/user", ErrorMiddleware(userHandler))
http.ListenAndServe(":8080", nil)
}

func userHandler(w http.ResponseWriter, r *http.Request) error {
userID := r.URL.Query().Get("id")
if userID == "" {
return &APIError{
StatusCode: http.StatusBadRequest,
Message: "缺少用户ID",
}
}

// 处理用户请求...
return nil
}

适用场景:

  • Web 服务中的统一错误处理
  • 需要将错误转换为 HTTP 响应的场景
  • 集中错误日志记录

错误处理辅助工具

错误检查辅助函数

func Check(err error) {
if err != nil {
log.Fatalf("致命错误: %v", err)
}
}

func CheckWithContext(err error, context string) {
if err != nil {
log.Fatalf("%s: %v", context, err)
}
}

func main() {
file, err := os.Open("data.txt")
CheckWithContext(err, "打开文件失败")
defer file.Close()

// 其他操作...
}

适用场景:

  • 简单脚本或快速原型
  • 错误需要立即终止程序的情况
  • 避免重复的错误检查代码

延迟错误处理(defer)

func ProcessFile(filename string) (err error) {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
closeErr := file.Close()
if closeErr != nil && err == nil {
err = fmt.Errorf("文件关闭失败: %w", closeErr)
}
}()

// 处理文件内容...
return nil
}

适用场景:

  • 资源清理操作(关闭文件、数据库连接等)
  • 需要将多个错误合并处理的场景
  • 确保资源正确释放的情况下处理可能的错误

错误重试机制

func Retry(attempts int, delay time.Duration, fn func() error) error {
var err error
for i := 0; i < attempts; i++ {
err = fn()
if err == nil {
return nil
}

if i < attempts-1 {
time.Sleep(delay)
delay *= 2 // 指数退避
}
}
return fmt.Errorf("操作失败(尝试 %d 次): %w", attempts, err)
}

func main() {
err := Retry(3, 1*time.Second, func() error {
return CallUnstableAPI()
})

if err != nil {
log.Printf("最终失败: %v", err)
}
}

适用场景:

  • 网络请求等可能临时失败的操作
  • 数据库连接等可能暂时不可用的资源
  • 需要指数退避策略的重试场景

错误处理最佳实践

错误处理原则

  1. 尽早处理:在错误发生的地方或最近的地方处理
  2. 明确上下文:为错误添加足够的信息
  3. 避免忽略错误:不要使用 _ 忽略错误
  4. 错误应可追溯:使用错误包装保持原始错误信息
  5. 区分错误类型:使用自定义错误或哨兵错误区分不同错误情况

错误日志记录

func LogError(err error, context ...string) {
if err == nil {
return
}

msg := "错误发生"
if len(context) > 0 {
msg = context[0]
}

// 使用结构化日志记录
log.WithFields(log.Fields{
"error": err.Error(),
"context": context,
"stack": getStackTrace(), // 自定义获取堆栈信息
}).Error(msg)
}

func main() {
if err := CriticalOperation(); err != nil {
LogError(err, "执行关键操作失败")
// 其他处理...
}
}

错误处理策略矩阵

错误类型 处理策略 示例
可恢复错误 重试、回退、降级 网络请求失败、文件锁定
输入错误 验证并返回给用户 表单验证失败、无效参数
配置错误 启动时检查并终止 缺少必要配置、无效配置
代码错误 记录日志并修复 空指针解引用、逻辑错误
外部依赖错误 隔离并降级 第三方服务不可用
资源不足 扩容或限制 内存不足、磁盘空间不足

特定场景的错误处理

并发错误处理

func ProcessConcurrently(jobs []Job) error {
var wg sync.WaitGroup
errCh := make(chan error, len(jobs))

for _, job := range jobs {
wg.Add(1)
go func(j Job) {
defer wg.Done()
if err := j.Process(); err != nil {
errCh <- fmt.Errorf("处理作业 %s 失败: %w", j.ID, err)
}
}(job)
}

// 等待所有作业完成
go func() {
wg.Wait()
close(errCh)
}()

// 收集所有错误
var errs []error
for err := range errCh {
errs = append(errs, err)
}

if len(errs) > 0 {
return fmt.Errorf("%d 个作业失败: %w", len(errs), errors.Join(errs...))
}

return nil
}

数据库事务错误处理

func TransferMoney(ctx context.Context, db *sql.DB, from, to string, amount int) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("开始事务失败: %w", err)
}

// 延迟回滚(如果事务未提交)
defer func() {
if err != nil {
tx.Rollback()
}
}()

// 扣款
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from); err != nil {
return fmt.Errorf("扣款失败: %w", err)
}

// 存款
if _, err := tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to); err != nil {
return fmt.Errorf("存款失败: %w", err)
}

// 提交事务
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交事务失败: %w", err)
}

return nil
}

HTTP 客户端错误处理

func GetWithRetry(ctx context.Context, url string, retries int) ([]byte, error) {
var body []byte
err := Retry(retries, 1*time.Second, func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err // 不可重试的错误
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // 网络错误,可重试
}
defer resp.Body.Close()

if resp.StatusCode >= 500 {
return fmt.Errorf("服务器错误: %s", resp.Status) // 可重试的错误
}

if resp.StatusCode >= 400 {
return fmt.Errorf("客户端错误: %s", resp.Status) // 不可重试的错误
}

body, err = io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %w", err)
}

return nil
})

return body, err
}

错误处理工具和库

标准库工具

// 1. 错误链检查
if errors.Is(err, sql.ErrNoRows) {
// 处理无结果错误
}

// 2. 错误类型提取
var validationErr ValidationError
if errors.As(err, &validationErr) {
// 处理验证错误
}

// 3. 错误解包
originalErr := errors.Unwrap(err)

// 4. 错误合并
combinedErr := errors.Join(err1, err2, err3)

第三方库推荐

pkg/errors:提供强大的错误包装和堆栈跟踪

import "github.com/pkg/errors"

func LoadConfig() error {
_, err := os.Open("config.yaml")
if err != nil {
return errors.Wrap(err, "打开配置文件失败")
}
// ...
}

go.uber.org/multierr:处理多个错误

import "go.uber.org/multierr"

func CloseAll(closers []io.Closer) error {
var err error
for _, closer := range closers {
err = multierr.Append(err, closer.Close())
}
return err
}

sentry-go:错误监控和报警

import "github.com/getsentry/sentry-go"

func init() {
sentry.Init(sentry.ClientOptions{
Dsn: "your-sentry-dsn",
})
}

func main() {
if err := CriticalOperation(); err != nil {
sentry.CaptureException(err)
log.Fatal(err)
}
}

错误处理反模式

错误忽略

// 反模式:忽略错误
file, _ := os.Open("important.txt")
file.Write(data) // 可能失败但错误被忽略

过度泛化的错误处理

// 反模式:所有错误都同样处理
result, err := ProcessData()
if err != nil {
log.Fatal("发生错误") // 丢失错误细节
}

错误信息暴露

// 反模式:向用户暴露内部细节
err := db.QueryRow("SELECT * FROM users").Scan(&user)
if err != nil {
// 不安全:暴露数据库结构
http.Error(w, "数据库错误: "+err.Error(), 500)
}

错误处理位置不当

// 反模式:在错误发生处不处理,传播过多层
func A() error {
return B()
}

func B() error {
return C()
}

func C() error {
return errors.New("具体错误")
}

// 在顶层处理时,丢失上下文

errors.Is和errors.As

在 Go 语言中,errors.Iserrors.As 是用于处理错误链的核心函数(自 Go 1.13 引入)。它们能递归检查被包装(wrapped)的错误,适用于现代 Go 错误处理模式。以下是详细对比和使用场景:

errors.Is

作用:检查错误链中是否存在特定值的错误(常与哨兵错误配合)

签名:

func Is(err, target error) bool

使用场景:

检查预定义的哨兵错误(Sentinel Errors) 如 io.EOFos.ErrNotExist 等。

if errors.Is(err, os.ErrNotExist) {
// 处理文件不存在的逻辑
}

检查自定义哨兵错误

var ErrInvalidInput = errors.New("invalid input")
// ...
if errors.Is(err, ErrInvalidInput) {
// 处理无效输入
}

递归检查被包装的错误 即使错误被多层 fmt.Errorf("... %w ...", err) 包装,也能穿透检查。

err := fmt.Errorf("layer2: %w", 
fmt.Errorf("layer1: %w",
os.ErrPermission))
errors.Is(err, os.ErrPermission) // true

errors.As

作用:从错误链中提取特定类型的错误(常与自定义错误类型配合)。

签名:

func As(err error, target any) bool

使用场景:提取自定义错误类型的详细信息当错误类型包含额外字段(如状态码、上下文)时。

type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string { return e.Message }

// 使用:
var me *MyError
if errors.As(err, &me) {
fmt.Println("错误代码:", me.Code) // 访问扩展字段
}

处理实现了某接口的错误 ,例如提取 net.Error 类型错误:

var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
// 处理网络超时
}

穿透多层错误包装提取类型 类似 errors.Is,但目标是类型而非值。

err := fmt.Errorf("wrapper: %w", &MyError{Code: 404})
var customErr *MyError
errors.As(err, &customErr) // true, customErr.Code == 404

关键区别总结

特性 errors.Is errors.As
目标 错误值(哨兵错误) 错误类型(自定义错误结构体/接口)
返回值 bool(是否匹配) bool(是否成功提取)
典型用途 检查特定错误是否出现 提取错误中的附加数据
参数 target error 类型(如 os.ErrNotExist 指针(如 &MyError{}&netErr

使用注意事项

errors.Astarget 必须是指针:

// 正确:
var e *MyError
errors.As(err, &e) // 注意:第二个参数是指针的指针

// 错误:
errors.As(err, e) // 编译失败,target 需为指针

优先使用 errors.Is/As 而非 == 或类型断言 。传统方式无法处理被包装的错误:

// 旧方法(不推荐):
if err == os.ErrNotExist { ... } // 无法处理包装错误
if e, ok := err.(*MyError); ok { ... } // 同样无法穿透包装

自定义错误需实现 Unwrap() error 方法 。若自定义错误需要被包装,需实现该方法以便 errors.Is/As 能递归检查:

type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }
func (e *MyError) Unwrap() error { return e.InnerError } // 返回内部错误

实践示例

// 定义哨兵错误和自定义类型
var ErrAuthFailed = errors.New("auth failed")

type DatabaseError struct {
Query string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("query %s failed: %v", e.Query, e.Err)
}
func (e *DatabaseError) Unwrap() error { return e.Err }

// 模拟错误链
err := fmt.Errorf("service failed: %w",
&DatabaseError{
Query: "SELECT * FROM users",
Err: ErrAuthFailed, // 包装哨兵错误
})

// 检查哨兵错误
if errors.Is(err, ErrAuthFailed) { // true
fmt.Println("认证失败")
}

// 提取自定义类型
var dbErr *DatabaseError
if errors.As(err, &dbErr) { // true
fmt.Println("失败查询:", dbErr.Query) // 输出: SELECT * FROM users
}

另一个示例

package main

import (
"errors"
"fmt"
)

// 自定义错误类型
type MyError struct {
Code int
Message string
// 添加一个内部错误字段,用于错误链
Err error
}

// 实现error接口
func (e *MyError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}

// 正确的Unwrap实现,返回内部错误而非自身
func (e *MyError) Unwrap() error {
return e.Err // 返回内部错误,而非自身,否则会卡死
}

var ErrNotFound = errors.New("not found")

func main() {
// 创建错误链:MyError包装ErrNotFound
err := &MyError{
Code: 404,
Message: "resource not available",
Err: ErrNotFound, // 包装标准错误
}

// 场景1: 判断是否为特定错误
if errors.Is(err, ErrNotFound) {
fmt.Println("错误匹配: Not Found") // 现在可以正确匹配
}

// 场景2: 提取自定义错误类型
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("错误类型匹配: Code=%d, Message=%s\n", myErr.Code, myErr.Message)
}
}

运行结果如下:

错误匹配: Not Found
错误类型匹配: Code=404, Message=resource not available

若不实现Unwrap

package main

import (
"errors"
"fmt"
)

// 自定义错误类型
type MyError struct {
Code int
Message string
// 添加一个内部错误字段,用于错误链
Err error
}

// 实现error接口
func (e *MyError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}

var ErrNotFound = errors.New("not found")

func main() {
// 创建错误链:MyError包装ErrNotFound
err := &MyError{
Code: 404,
Message: "resource not available",
Err: ErrNotFound, // 包装标准错误
}

// 场景1: 判断是否为特定错误
if errors.Is(err, ErrNotFound) {
fmt.Println("错误匹配: Not Found") // 现在可以正确匹配
}

// 场景2: 提取自定义错误类型
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("错误类型匹配: Code=%d, Message=%s\n", myErr.Code, myErr.Message)
}
}

则运行结果如下:

错误类型匹配: Code=404, Message=resource not available

选择原则

  • 需要检查特定错误值(如 io.EOF) → errors.Is
  • 需要访问错误中的结构化数据(如错误码、上下文) → errors.As
  • 若错误是简单字符串,无需额外数据 → 直接使用 err.Error()fmt 打印