4.2 错误处理:error接口、包装与自定义


4.2 错误处理:error 接口、包装与自定义

本节摘要:Go 把错误做成普通值——内置 error 接口只有一个返回字符串的方法,函数以"结果加 error"的签名显式传递失败。本节覆盖 errors.New 与格式化构造、if err 检查模式、百分号 w 的错误包装链、errors.Is 与 errors.As 的两种解包、自定义错误类型携带结构化信息,以及错误处理与 panic 的分界线。

本节目标

阅读完本节,你应当能够:

  1. 写出符合惯例的"值加 error"函数签名并逐层检查;
  2. 用百分号 w 包装错误保留原始链;
  3. 区分 errors.Is(哨兵值匹配)与 errors.As(类型匹配)的使用场景;
  4. 定义携带字段的自定义错误类型并支持解包;
  5. 判断一个失败该走 error 还是 panic。

一、error 就是一个接口

type error interface { Error() string }

任何实现了返回字符串方法的类型都是 error。函数的惯例是把 error 放最后一个返回值,成功时为 nil。这个"简单到过分"的定义是整个错误处理体系的地基——因为它是接口,你可以在里面塞任何结构化信息(后面自定义错误类型会用到)。

构造错误的两把起子:

errors.New("除数不能为零") // 静态文本 fmt.Errorf("打开文件 %s 失败: %v", name, err) // 带格式化上下文

二、if err != nil:不是噪音,是流程图

result, err := divide(10, 2) if err != nil { fmt.Println("发生错误:", err) return } fmt.Println("结果:", result)

这套模式会出现在几乎每个函数里,初学者嫌它啰嗦,维护者爱它诚实:每个可能失败的点都被肉眼看见并当场决策——返回、重试、降级还是记日志,路径全在明面上。异常机制把失败路径藏进调用栈,Go 把它铺在主干道上。

处理姿势速查:

场景 姿势
本层能恢复 重试、降级、给默认值
本层加得上下文 包装后继续上抛
无法处理 原样上抛,别吞
绝不该发生 panic(见第 2 章)
只想记日志后继续 记完再显式走分支,别空 if

⚠️ 常见坑一:_ = err 把错误丢掉。三行代码没事,三千行时它是事故报告里最常见的遗骸。
⚠️ 常见坑二:if 块里 return 后,函数尾部还继续用"失败时的变量"。编译器不拦,逻辑已错。

三、包装与解包:错误链的两端

包装用格式化动词 %w(wrap)而不是 %v。区别在 %w 保留原始 error 的身份,链可以往下追:

func openFile(filename string) (*os.File, error) { file, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("打开文件 %s 失败: %w", filename, err) } return file, nil }

层层包装后,最终的错误信息像洋葱:"处理订单失败: 扣款失败: 打开文件 xx 失败: 权限不足"——每一层加一句当时才知道的上下文。

解包有两个标准函数,分工不同:

// Is:比对哨兵值——"这条链里有没有这个具体的错误实例?" if errors.Is(err, os.ErrNotExist) { // 文件不存在,走创建流程 } // As:找类型——"链里有没有这种类型的错误?取出来读字段。" var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Println(pathErr.Op, pathErr.Path) // 读出操作与路径 }

记法:Is 问"是不是它",As 问"是不是这类"。Is 用于标准库预定义的哨兵错误(文件不存在、上下文取消等);As 用于提取结构化字段做分支。链条中只要有一环用了 %v 而不是 %w,链就断在那里——Is 与 As 都穿透不过去。

四、自定义错误类型:把信息装进错误

字符串只能给人读,程序要分支就需要结构。自定义类型实现 Error 方法即可:

type ValidationError struct { Field string Reason string } func (e *ValidationError) Error() string { return "字段 " + e.Field + " 校验失败: " + e.Reason } func parseAge(s string) (int, error) { n, err := strconv.Atoi(s) if err != nil { return 0, &ValidationError{Field: "age", Reason: "不是整数"} } if n < 0 || n > 150 { return 0, &ValidationError{Field: "age", Reason: "超出范围"} } return n, nil } // 调用方 _, err := parseAge("abc") var ve *ValidationError if errors.As(err, &ve) { fmt.Println("问题字段:", ve.Field) // 结构化信息可直接用于响应 }

自定义类型的价值:错误从"日志素材"升级为"可编程的数据"。HTTP 层可以把 ValidationError 映射成 400 状态码加字段提示,而其他错误映射成 500——分支依据是类型,不是解析字符串。

工程建议:包对外导出少数哨兵错误(供 Is)与少数错误类型(供 As),别把内部错误类型全导出,否则调用方会依赖你的实现细节。

五、error 与 panic 的分界(回顾与深化)

第 2 章立过规矩,这里补上工程视角:可预期的失败永远 error——网络抖了、文件没了、用户输入错了,这些是正常业务宇宙的一部分;panic 留给不变量崩坏——数组越界、空指针、断言失败,意味着代码有 bug 或数据已腐坏。库作者替调用方 panic 是失礼,服务边缘用 recover 兜底是责任。两者混用的信号是:你开始在 catch 里写业务逻辑。

💡 关键直觉:把 error 当快递单号——每一层经手都签个名(包装加上下文),收件人随时能查全链路;把 panic 当火警——响了就疏散(recover 兜住转 500),没人拿火警做日常通勤。

六、错误流转全景

问答补遗。错误信息该小写还是大写开头? 惯例小写、不以标点收尾——它常被拼接进上层信息。一层包几层合适? 每层加一句"当时在干什么"即可,同一层反复包装同一句是噪音。error 能比较吗? 能 ==,但只对哨兵错误有意义;正确姿势是 Is。goroutine 里的错误怎么传出? 结果通道逐个送出(发送"错误加值"的小结构),主流程收集汇总——错误也是数据,通道照传。

一节小结

  • error 是接口:一个返回字符串的方法,可携带任意结构。
  • 惯例签名:error 放最后,成功为 nil。
  • 包装用 %w:保留链;%v 会切断 Is 与 As 的通路。
  • Is 比对哨兵值,As 取类型读字段,分别对应"是不是它/是不是这类"。
  • 自定义错误类型让错误成为可编程数据,支撑上层分支。
  • 导出面克制:哨兵与类型导出少数,内部细节勿泄漏。
  • error 管预期失败,panic 管崩坏,混用即设计味道。

下一节把这些能力装进一个像样的项目结构。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U