错误与资源清理
错误与资源清理 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这课让错误变得可检查、可追踪:用哨兵错误和 %w 包装携带上下文,用 errors.Is/As 做判断,用 defer 保证文件、响应体这类资源在出错路径上也一定被释放。
问题场景
书签服务要从本地 JSON 文件读种子数据。文件可能不存在、JSON 可能坏。Python 里 try/except 一把抓,Go 把每一步的错误显式递到你手上——好处是调用方能区分「没这个文件」和「文件坏了」并分别处理,前提是你得把错误包得有上下文,资源放得干净。
核心解释
先定哨兵错误,包装时用 %w 保留判断链:
var (
ErrNotFound = errors.New("bookmark not found")
ErrDuplicate = errors.New("bookmark already exists")
)
func (s *MemoryStore) Get(id int64) (Bookmark, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, ok := s.byID[id]
if !ok {
return Bookmark{}, fmt.Errorf("get bookmark %d: %w", id, ErrNotFound)
}
return b, nil
}
%w 把 ErrNotFound 包进新错误,形成 get bookmark 42: bookmark not found 这样的链。调用方用 errors.Is 穿透链条判断身份:
b, err := store.Get(42)
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "书签不存在", http.StatusNotFound)
case err != nil:
http.Error(w, "内部错误", http.StatusInternalServerError)
default:
json.NewEncoder(w).Encode(b)
}
errors.As 则用来「从链里取出具体错误类型」拿字段,比如驱动错误里取约束名。两者都只认 %w 包出来的链——用 %v 或字符串拼接包装,链就断了,errors.Is 返回 false,这是 Go 错误处理最经典的静默 bug。
资源清理靠 defer:它在函数返回前执行,无论从哪个 return 出去:
func loadSeed(path string) ([]Bookmark, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open seed %s: %w", path, err)
}
defer f.Close() // 必须在错误检查之后
var bs []Bookmark
if err := json.NewDecoder(f).Decode(&bs); err != nil {
return nil, fmt.Errorf("decode seed %s: %w", path, err)
}
return bs, nil
}
打开失败直接返回,Close 不会执行(f 是 nil,defer Close 反而会 panic);打开成功则 Close 一定执行,包括 Decode 出错的那条路径。逐层包装的错误信息叠起来就是排查线索:open seed data.json: no such file or directory。多个 defer 的执行顺序是后进先出,先申请的资源后释放,天然符合栈的形状。
示例拆解
三种错误路径逐一验证:
go run . # seed.json 不存在
# load seed failed: open seed seed.json: no such file or directory
echo "{bad" > seed.json && go run .
# load seed failed: decode seed seed.json: invalid character 'b' ...
echo '[{"id":1,"title":"Go","url":"https://go.dev","tags":["go"]}]' > seed.json && go run .
# 加载成功,List 输出 1 条
main 里只做一件事:bs, err := loadSeed("seed.json"); if err != nil { log.Fatalf("load seed failed: %v", err) }。注意 main 打印的是完整链条,不需要自己拼上下文——每一层都用 %w 把责任往下传,最上层拿到的就是全程线索。add 重复检测也顺手做了:URL 已存在时返回 fmt.Errorf("add bookmark: %w", ErrDuplicate),调用方 errors.Is 识别后翻译成友好提示。
动手任务
- 给 MemoryStore.Add 加重复检测:URL 已存在时返回包着 ErrDuplicate 的错误,调用方用 errors.Is 判断。
- 故意把 %w 改成 %v,重新跑 errors.Is 判断,观察它失效——亲眼看一次链断裂。
- 写 loadSeed 的调用方,用不存在、坏 JSON、正常三种文件各跑一遍,确认三种输出都能定位根因。
常见错误
- 用 %v 包装后再 errors.Is:永远 false,判断失灵且难发现,包装一律 %w。
- defer f.Close() 写在 err 判断之前:f 为 nil 时 Close 会 panic,defer 必须跟在错误检查后。
- 忘记 defer Close 就提前 return:文件句柄泄漏,程序跑久了报 too many open files。
- 吞掉错误只打日志不返回:调用方拿到零值继续跑,问题被推迟到更难查的地方。
检查与复盘
完成下方检查题。自查清单:
- 返回错误一律 fmt.Errorf + %w 带上下文,判断一律 errors.Is/As
- defer Close 紧跟在错误检查之后,不提前
- 最上层打印错误时保留完整链条,不再手工拼接上下文
参考资料
- Go Spec:defer 语句 —— 执行时机与求值规则
- 标准库 errors —— Is/As/New/Join 完整说明
- Go 博客:Go 中的错误处理 —— 错误值设计的官方论述
- 标准库 fmt.Errorf —— %w 动词的定义
读完这一课了?
标记完成后,工作台会安排接下来的学习。