讲解

Go 没有 try-catch 异常机制,错误就是普通的值:内置接口 error 只有一个方法 Error() string。函数可能失败时,约定把 error 作为最后一个返回值,调用方第一反应就是 if err != nil 处理它。这种「错误即值」的显式风格常被吐槽啰嗦,但它强迫每个错误都被正面处理,而不是被异常抛到九霄云外无人认领——大型系统的可靠性正来自这份啰嗦。

构造错误有三个层次:errors.New("描述") 造一个静态错误;fmt.Errorf("上下文: %v", x) 格式化造错误;fmt.Errorf("...: %w", err) 用 %w 动词「包装」已有错误,保留错误的来龙去脉。哨兵错误(如 var ErrNotFound = errors.New(...))作为包级导出变量,让调用方能判断「是不是这一类错误」。

判断错误用 errors.Is 和 errors.As,它们会自动拆包:errors.Is(err, ErrNotFound) 沿着包装链逐层比较;errors.As 则把链上第一个匹配的目标类型提取出来,配合自定义错误类型可以携带结构化信息(哪个字段、什么原因)。切记:不要用 == 比较错误,也不要解析 err.Error() 的文案来判断错误种类——文案会改,包装会断,Is/As 才是正道。

示例

返回与处理 error 的基本姿势:

package main

import (
	"errors"
	"fmt"
)

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

func main() {
	if q, err := divide(10, 2); err == nil {
		fmt.Println("结果:", q)
	}
	if _, err := divide(1, 0); err != nil {
		fmt.Println("出错了:", err)
	}
}

%w 包装错误,errors.Is 拆包判断:

package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("记录不存在")

func findUser(id int) error {
	if id <= 0 {
		return fmt.Errorf("查询用户 %d: %w", id, ErrNotFound)
	}
	return nil
}

func main() {
	err := findUser(-1)
	fmt.Println(err) // 查询用户 -1: 记录不存在
	if errors.Is(err, ErrNotFound) {
		fmt.Println("确认:这是「不存在」类错误")
	}
}

自定义错误类型与 errors.As:

package main

import (
	"errors"
	"fmt"
)

type ValidationError struct {
	Field   string
	Message string
}

func (e *ValidationError) Error() string {
	return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Message)
}

func checkAge(age int) error {
	if age < 0 {
		return &ValidationError{Field: "age", Message: "不能为负数"}
	}
	return nil
}

func main() {
	err := checkAge(-5)
	var ve *ValidationError
	if errors.As(err, &ve) {
		fmt.Println("出问题的字段:", ve.Field)
	}
	fmt.Println(err)
}

常见坑

  • 用 _ 吞掉 error:result, _ := mayFail() 是 Go 代码里最危险的习惯,错误被丢弃后程序拿着零值继续跑,问题被拖到很远的地方才爆炸。每个 error 要么处理,要么显式向上返回。
  • 用 == 或字符串匹配判断错误:err == ErrNotFound 在错误被 %w 包装后就失效了;strings.Contains(err.Error(), "not found") 更是脆弱不堪。判断用 errors.Is,提取用 errors.As。
  • %w 与 %v 混用:fmt.Errorf 里 %w 保留错误链、%v 只是格式化文案。想让调用方能 Is/As 就必须用 %w,且一个 Errorf 里 %w 只能有一个(Go 1.20 起支持多个)。
  • 成功路径返回非 nil 的自定义 error 接口:return nil 才安全;return 一个类型为 *MyError 的 nil 值会让 err != nil 判断为 true(接口持有类型信息),这是著名的「nil 接口陷阱」。
  • 错误文案大写开头或带句号:Go 社区约定错误文案小写开头、不带结尾标点,因为错误会被逐层包装拼接:「查询用户: 打开配置: 文件不存在」,大写和句号会让链条读起来很怪。

小结

error 是值,多返回值返回,if err != nil 处理;errors.New/Errorf 造错误,%w 包装,Is/As 拆包判断。下一节学习与错误处理配套的执行控制:defer、panic 与 recover。