G04第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

错误与资源清理

错误与资源清理 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这课让错误变得可检查、可追踪:用哨兵错误和 %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 识别后翻译成友好提示。

动手任务

  1. 给 MemoryStore.Add 加重复检测:URL 已存在时返回包着 ErrDuplicate 的错误,调用方用 errors.Is 判断。
  2. 故意把 %w 改成 %v,重新跑 errors.Is 判断,观察它失效——亲眼看一次链断裂。
  3. 写 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 紧跟在错误检查之后,不提前
  • 最上层打印错误时保留完整链条,不再手工拼接上下文

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(Go 1.22+,go run 可直接运行)。输出什么?

package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("not found")

func load(id string) error {
	return fmt.Errorf("load book %q: %w", id, ErrNotFound)
}

func main() {
	err := load("b1")
	fmt.Println(errors.Is(err, ErrNotFound))
	fmt.Println(err)
}
Q2单选中等

需要从错误链里取出自定义错误类型 ParsingError 的字段(如 Line 行号)做分支处理。正确的做法是?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...