前言
Go 没有 try/catch,错误是返回值,必须逐个处理。初看啰嗦,实则强迫你思考每一个失败路径。这篇把现代 Go(1.13+)的错误处理体系讲全。
一、error 是什么
// 标准库定义: 就一个接口
type error interface {
Error() string
}
// 最常见的形态
func ReadConfig(path string) ([]byte, error) {
return os.ReadFile(path) // os.ReadFile 的签名: ( []byte, error)
}
func main() {
data, err := ReadConfig("app.conf")
if err != nil {
log.Fatal(err) // open app.conf: no such file or directory
}
fmt.Println(len(data))
}
铁律:err != nil 检查永远紧跟在产生它的调用后;不要用 _ 吞掉错误(除非明确说明为何可忽略)。
二、创建错误与错误包装
import (
"errors"
"fmt"
)
// 创建
err1 := errors.New("disk full")
err2 := fmt.Errorf("port %d 非法", 808000) // 带格式化
// 包装(wrap): 加上下文, 同时保留根因 —— %w 是关键!
if err := connect(db); err != nil {
return fmt.Errorf("init db: %w", err)
}
// 输出: init db: dial tcp 10.0.0.2:3306: i/o timeout
// 内部仍可透传判断出根因
%v vs %w:%v 只是拼接文字(丢失类型信息);%w 形成错误链,允许 errors.Is/As 沿链查找。
三、errors.Is / As / Join
// sentinel 错误: 预定义的标志性错误值
var ErrNotFound = errors.New("not found")
func GetUser(id int) error {
// ... 查库失败时: return fmt.Errorf("get user %d: %w", id, ErrNotFound)
}
if err := GetUser(42); err != nil {
if errors.Is(err, ErrNotFound) { // 沿错误链找相同值
// 处理"不存在": 返回 404 等
}
// 其他错误继续上抛
}
// 判断具体类型(结构化错误)
type TimeoutError struct {
Op string
Dur time.Duration
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("%s timed out after %s", e.Op, e.Dur)
}
var te *TimeoutError
if errors.As(err, &te) { // 沿链找可赋值给 *TimeoutError 的
log.Printf("操作 %s 超时, 时长 %s", te.Op, te.Dur)
}
// 合并多个错误(并行任务汇总场景, go1.20+)
var errs []error
for _, u := range urls {
errs = append(errs, check(u))
}
if err := errors.Join(errs...); err != nil { // nil 会被跳过
return err // 多行错误信息一次输出
}
四、自定义错误类型
// 好的错误类型: 携带结构化上下文
type HTTPError struct {
StatusCode int
URL string
Err error // 内层错误
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("http %s: %d: %v", e.URL, e.StatusCode, e.Err)
}
func (e *HTTPError) Unwrap() error { // 支持 errors.Is/As 沿链下钻
return e.Err
}
func Fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, &HTTPError{URL: url, Err: err} // 500? 不, 网络层错
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, &HTTPError{URL: url, StatusCode: resp.StatusCode}
}
return io.ReadAll(resp.Body)
}
// 调用方精确处理
var he *HTTPError
if errors.As(err, &he) && he.StatusCode == 429 {
time.Sleep(time.Second) // 限流: 退避重试
}
五、panic / recover:留给真正的异常
Go 中 error 是业务的一部分,panic 是程序员的 bug:
func mustParse(cfg string) Config {
c, err := parse(cfg)
if err != nil {
panic(fmt.Sprintf("配置无法解析: %v", err)) // 程序启动期的不变量被破坏
}
return c
}
// recover: 只在 defer 中生效, 用于兜底防止进程崩溃
func safeCall() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
panic("boom")
}
什么时候用 panic:
- ✅ 程序初始化阶段的致命错误(配置错误、依赖缺失)——快速失败
- ✅ 库内部不变量被破坏(不可能的分支)
- ❌ 日常业务错误(文件不存在、网络超时)——用 error
- ❌ 通过 panic 跨层传业务错误——性能差且语义混乱
六、工程化惯例
// 1. 包级 sentinel 错误集中定义(Err 前缀)
package store
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)
// 2. 返回错误时永远 wrap 上下文
if err := s.db.Query(...); err != nil {
return fmt.Errorf("get user %d: %w", id, err)
}
// 3. 最外层统一记录(带堆栈)
log.Printf("handle error: %+v", err) // %+v 打印详细信息
// 4. 处理不了的错误, 原样返回, 不要重复包装
错误信息规范:小写开头、不以标点结尾、包含关键参数(get user 42: connection refused)。
七、与 Python 异常的对比
| 维度 | Go error | Python exception |
|---|---|---|
| 传递方式 | 返回值,显式逐层处理 | 抛出,隐式向上冒泡 |
| 忽略风险 | _ = err 显式写出来 |
裸 except 静默吞掉 |
| 分类判断 | errors.Is/As 沿链查 | except 具体类型 |
| 崩溃机制 | panic(仅 bug 级) | 未捕获异常即崩溃 |
| 样板代码 | 多(if err != nil) | 少 |
啰嗦是特性不是缺陷——每个错误都在你眼前过一遍。
小结
| 工具 | 用途 |
|---|---|
fmt.Errorf("...: %w", err) |
包装 + 保留根因 |
errors.Is |
判断链上是否有 sentinel |
errors.As |
提取链上特定类型 |
errors.Join |
汇总多个错误 |
| 自定义类型 + Unwrap | 结构化错误 |
| panic/recover | 只留给 bug 与初始化失败 |
本文是「Golang」系列第 6 篇。