Go 错误处理

error 接口、包装与解包、errors.Is/As、panic 与 recover。

Intermediate
Key Points错误是值,用 `%w` 包装保留链条,用 `errors.Is/As` 判断而不是比较字符串。

error 是接口

Go
type error interface {
	Error() string
}
Go
import "errors"

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("除数不能为零")
	}
	return a / b, nil
}

r, err := divide(1, 0)
if err != nil {
	fmt.Println("错误:", err)
}
Output
错误: 除数不能为零

自定义错误

Go
type NotFoundError struct {
	Resource string
	ID       int
}

func (e *NotFoundError) Error() string {
	return fmt.Sprintf("%s %d 不存在", e.Resource, e.ID)
}

func findUser(id int) error {
	return &NotFoundError{Resource: "用户", ID: id}
}

err := findUser(7)
fmt.Println(err)
Output
用户 7 不存在

包装与解包

Go
var ErrNotFound = errors.New("未找到")

func loadConfig() error {
	return fmt.Errorf("读取配置失败: %w", ErrNotFound)   // %w 包装
}

func main() {
	err := loadConfig()
	fmt.Println(err)                      // 完整信息
	fmt.Println(errors.Is(err, ErrNotFound))  // true

	// 取出原始错误
	fmt.Println(errors.Unwrap(err))
}
Output
读取配置失败: 未找到
true
未找到

%w and %v differ in one way: %w keeps the wrapped error in the chain so errors.Is can find it. Use %w whenever you need to test the error later.

errors.Is 与 errors.As

Go
// Is:判断是不是某个「哨兵错误」
if errors.Is(err, os.ErrNotExist) {
	fmt.Println("文件不存在")
}

// As:判断是不是某个「错误类型」,并取出来用
var nf *NotFoundError
if errors.As(err, &nf) {
	fmt.Println("找不到", nf.Resource, nf.ID)
}
FunctionComparesUse when
errors.Is错误值(哨兵)ErrNotFound 这类预定义错误
errors.As错误类型要访问自定义错误的字段
==直接比较只在不包装时有效
Never test with err.Error() == "not found". A wording change breaks it, and wrapping lengthens the message. One of the most common Go review comments.

panic 与 recover

Go
func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic: %v", r)
		}
	}()
	return a / b, nil            // b=0 时 panic
}

r, err := safeDivide(1, 0)
fmt.Println(r, err)

r, err = safeDivide(10, 2)
fmt.Println(r, err)
Output
0 panic: runtime error: integer divide by zero
5 <nil>

panic is for the program cannot continue (out of range, nil deref, failed init); error is for expected failures (missing file, validation). In business code 99% of errors should be error.

错误处理的三条实践

  1. 不要忽略 —— _ = doSomething() 是技术债,至少记日志
  2. 加上下文 —— fmt.Errorf("加载用户 %d: %w", id, err)
  3. 只处理一次 —— 要么记录要么返回,不要既记录又返回(日志会重复)
Go
// 反例:日志重复,一层层打同一句话
func a() error {
	err := b()
	if err != nil {
		log.Println(err)
		return err
	}
	return nil
}

// 正例:底层包装上下文,顶层统一记录
func loadUser(id int) error {
	u, err := db.Find(id)
	if err != nil {
		return fmt.Errorf("查询用户 %d: %w", id, err)
	}
	_ = u
	return nil
}

Friends & Partners