8 错误

Go 语言错误处理详解

8.1 接口 error 是什么

在 C 语言中,通常使用整数错误码(errno)来表示函数处理出错,用 -1 表示错误,0 表示正确。

在 Go 中,使用 error 类型来表示错误,它是一个接口类型:

type error interface {
    Error() string
}

创建 error 的方式

1. errors.New()

// src/errors/errors.go
func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

使用 errors.New() 创建的错误实际上是 errors 包里未导出的 errorString 类型,包含唯一字段 s,实现了 Error() string 方法。

2. fmt.Errorf()

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, fmt.Errorf("math: square root of negative number %g", f)
    }
    // implementation
}

fmt.Errorf 先将字符串格式化,再调用 errors.New() 创建错误。

重要原则: 返回错误的函数要给出具体的"上下文"信息,如上例中要给出负数到底是什么值。

使用 error 的约定

  • 将 error 放在函数返回值的最后一个
  • 构造 error 时,传入的字符串首字母小写,结尾不带标点符号
err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)

8.2 接口 error 有什么问题

优点

Go 从语言层面要求明确处理错误,而不是像 Java 使用 try-catch-finally。

缺点

代码中"error"满天飞,显得冗长拖沓,容易掩盖正常逻辑。

err := doStuff1()
if err != nil {
    // handle error...
}
err = doStuff2()
if err != nil {
    // handle error...
}
err = doStuff3()
if err != nil {
    // handle error...
}

官方观点

Russ Cox(Go 作者之一)

  • 返回值错误处理机制适用于大型软件,try-catch 更适合小程序

Go 官网 FAQ

  • try-catch 会让代码变得混乱
  • 程序员倾向将常见错误也抛到异常里,使错误处理更冗长且易出错
  • Go 的多返回值使返回错误异常简单
  • 普通错误使用 error,真正异常使用 panic-recover

承认:Go 的错误处理机制对开发人员确实有一定的心智负担。


8.3 如何理解关于 error 的三句谚语

8.3.1 视错误为值

箴言:Errors are just values(错误就是值)

只要实现了 Error 接口的类型都可以认为是 Error。

处理 error 的三种方式

1. Sentinel errors(哨兵错误)

预先约定好的错误,处理流程必须在此停下。

func main() {
    r := bytes.NewReader([]byte("0123456789"))
    _, err := r.Read(make([]byte, 10))
    if err == io.EOF {
        log.Fatal("read failed:", err)
    }
}

问题

  • 必须在定义 error 和使用 error 的包之间建立依赖关系
  • 当需要返回带上下文信息的错误时,调用者不得不使用字符串匹配方式判断错误类型(代码坏味道)
func readfile(path string) error {
    err := openfile(path)
    if err != nil {
        return fmt.Errorf("cannot open file %s: %v", path, err)
    }
    return nil
}

// 调用者需要字符串匹配,这是坏味道
if strings.Contains(error.Error(), "not found") {
    // handle error
}

建议:尽量避免 Sentinel errors


2. Error Types

实现了 error 接口的类型,可以附带额外字段提供更多信息。

// src/os/error.go
type PathError struct {
    Op   string
    Path string
    Err  error
}

外层调用者使用类型断言判断错误:

func underlyingError(err error) error {
    switch err := err.(type) {
    case *PathError:
        return err.Err
    case *LinkError:
        return err.Err
    case *SyscallError:
        return err.Err
    }
    return err
}

问题:同样存在引入包依赖的问题

建议:不要把 Error types 作为导出类型


3. Opaque errors(黑盒错误)

只知道错误发生了,但看不到内部细节。

func fn() error {
    x, err := bar.Foo()
    if err != nil {
        return err
    }
    // use x
    return nil
}

策略:一旦出错,直接返回错误;否则继续正常流程。

高级用法:判断错误是否具有某种行为(接口),而不是具体类型

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}

优点

  • 不需要 import 定义错误的包
  • 不需要知道 error 的具体类型
  • 面向接口编程

8.3.2 检查并优雅地处理错误

箴言:Don't just check errors, handle them gracefully

错误示例

func AuthenticateRequest(r *Request) error {
    err := authenticate(r.User)
    if err != nil {
        return err
    }
    return nil
}

应优化为:

func AuthenticateRequest(r *Request) error {
    return authenticate(r.User)
}

添加上下文信息的问题

func AuthenticateRequest(r *Request) error {
    err := authenticate(r.User)
    if err != nil {
        return fmt.Errorf("authenticate failed: %v", err)
    }
    return nil
}

这种做法破坏了相等性检测,无法判断错误是否是预先定义好的错误。

解决方案:pkg/errors

Go 1.13 之前使用 github.com/pkg/errors,Go 1.13 后自带了 error 高级功能,但输出堆栈仍推荐前者。

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error

// Cause unwraps an annotated error.
func Cause(err error) error

使用示例

func ReadFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, errors.Wrap(err, "open failed")
    }
    defer f.Close()
    
    buf, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, errors.Wrap(err, "read failed")
    }
    return buf, nil
}

func ReadConfig() ([]byte, error) {
    home := os.Getenv("HOME")
    config, err := ReadFile(filepath.Join(home, ".settings.xml"))
    return config, errors.Wrap(err, "could not read config")
}

func main() {
    _, err := ReadConfig()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

输出

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

使用 %+v 输出堆栈

fmt.Printf("%+v", err)

输出详细的错误堆栈信息。

Cause 函数示例

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := errors.Cause(err).(temporary)
    return ok && te.Temporary()
}

8.3.3 只处理错误一次

箴言:Only handle error once

什么是"处理"错误

Handling an error means inspecting the error value, and making a decision.

即检查错误并做出一个决定。

错误示例 1:忽略错误

func Write(w io.Writer, buf []byte) {
    w.Write(buf)  // 忽略了返回的错误
}

错误示例 2:处理两次错误

func Write(w io.Writer, buf []byte) error {
    _, err := w.Write(buf)
    if err != nil {
        // 第一次处理:写日志
        log.Println("unable to write:", err)
        // 第二次处理:返回错误
        return err
    }
    return nil
}

问题

  • 日志文件中有重复的错误描述
  • 最上层调用者拿到的错误缺少上下文信息

8.4 错误处理的改进

Go 1.13 错误包裹(wrapping)

Go 1.13 支持 error wrapping,通过提供 Unwrap 方法实现。

特性

  • fmt.Errorf 增加了 %w 格式符
  • error 包增加了三个函数:errors.Unwraperrors.Iserrors.As

fmt.Errorf 使用 %w

// 嵌套 error
err := fmt.Errorf("open failed: %w", originalErr)

errors.Unwrap

返回被嵌套的下一层 error,需要多次调用才能获取最里层的 error。

func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

errors.Is

判断 err 是否和 target 是同一类型,或者 err 嵌套的 error 有没有和 target 是同一类型的。

func Is(err, target error) bool {
    if target == nil {
        return err == target
    }
    isComparable := reflectlite.TypeOf(target).Comparable()
    
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

errors.As

从 err 错误链里找到第一个和 target 相等的值,并设置 target 所指向的变量为 err。

func As(err error, target interface{}) bool {
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    
    if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    
    targetType := typ.Elem()
    for err != nil {
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
            return true
        }
        err = Unwrap(err)
    }
    return false
}

注意事项

如果 target 不是一个指向实现了 error 接口的类型或者其他接口类型的非空指针,函数会 panic。


总结

概念 说明
error 接口 包含 Error() string 方法
创建错误 errors.New()fmt.Errorf()
Sentinel errors 预定义错误,如 io.EOF,避免过度使用
Error Types 自定义错误类型,避免导出
Opaque errors 黑盒错误,通过行为(接口)判断
错误处理三原则 视错误为值、优雅处理、只处理一次
Go 1.13 改进 %wUnwrapIsAs
posted @ 2026-03-30 15:23  cyusouyiku  阅读(2)  评论(0)    收藏  举报