本层判断标准

把这段代码里所有失败路径列一遍,每一条是不是都有人负责?

答「否」,说明有失败被丢掉或被打断。丢掉失败的写法在测试里也能全绿,只是因为没触发那条路径。

已经交给检查工具的四条

下面这几条有确定判据,写进配置,不需要评审。它们的共同点是:修法只有一种,所以能自动化。

拦什么规则命令
未处理的 errorGo errcheckgolangci-lint run
裸 except:、可变默认参数、eval 与 execPython E722、B006、S307、S102ruff check --select E722,B006,S307,S102 .
var、空的 catch、被重新赋值却声明为 const 的反面ESLint no-var、prefer-const、no-empty,都在 eslint:recommended 里npm --prefix frontend run lint
init() 里做 IO、goroutine 泄漏、未取消的 contextgolangci-lint 的 gochecknoinits、contextcheck,以及 govet 的 lostcancelgolangci-lint run
库代码里的 unwrap() 与 expect()clippy::unwrap_used,默认关闭,需要显式打开cargo clippy -- -D warnings -W clippy::unwrap_used

.golangci.yml(在格式化配置上增加四条)

这几条与命名那页的 revive 写在同一个文件里,不要另建一份。

version: "2"

linters:
  enable:
    - errcheck
    - govet
    - ineffassign
    - staticcheck
    - unused
    - revive
    - gochecknoinits
    - contextcheck

  settings:
    revive:
      rules:
        - name: var-naming
          severity: error

pyproject.toml(追加 B、S 与 E722)

同样在基线之上追加。E722(裸 except)已经在基线的 E 组里,不用重复写;这一页只加 B(bugbear)与 S(bandit)。加上之后先跑一次数现有代码有多少命中,一次改不完就先只开 B006 与 E722。

[tool.ruff.lint]
# 基线:select = ["E", "F", "I", "B", "S", "C901"]
extend-select = ["B", "S"]

工具查不到的边界:S307(eval)在测试与数据生成的脚本目录里会误报,可以在 pyproject.toml 里对 scripts/ 单独关掉,但 src/ 下不关。

禁用清单

「禁用」的含义是:评审的人看到就挡回,不用讨论。替代写法都是同一件事——把失败交还给调用方。

特性理由替代写法规则
Go:f, _ := os.Open(path)f 为 nil 时下一条语句才报错,栈里看不出根因写 f, err := os.Open(path),判断后 fmt.Errorf("open notes: %w", err)errcheck
Go:用 panic 处理输入错误打掉整个进程或整个请求,其他用户的请求一起失败返回 error,由路由层统一转成状态码人工,见接口
Go:time.Sleep 当同步用掩盖竞态,测试结果随机用 sync.WaitGroup 或 channel 等待,要超时就配 context.WithTimeout人工
Go:init() 里连数据库、读配置启动顺序不可控,测试里没法替换依赖写成显式构造函数,在 main 里装配gochecknoinits
Python:裸 except:连 KeyboardInterrupt 一起吞掉,失败变成静默的错误结果写 except ValueError as err:,只捕具体异常E722
Python:def f(items=[])默认值只在定义时求值一次,多次调用共用同一个列表def f(items=None),函数内 items = [] if items is None else itemsB006
Python:eval()、exec()把输入当代码执行解析用 ast.literal_eval,配置用 json.loads 或 tomllibS307、S102
JS/TS:var函数作用域、可重复声明,容易写出意料外的覆盖默认写 const,要重新赋值时写 letno-var、prefer-const
JS/TS:空 catch {}失败被静默记录后重新抛出,或返回一个能判断的失败值no-empty
Rust:库代码里 unwrap()输入不合法时 panic返回 Result,只在测试里用 unwrapclippy::unwrap_used
Java:只用 try 不写 catch 也不写 finally,把异常一路抛到最外层调用方分不清是哪一步失败的在能补充上下文的那一层包一层并重抛,或在最外层统一转成响应人工
可以这么做 把失败包装后往上传:Go 用 %w 保留原始错误,Python 用 raise ... from err,Java 保留 cause。这样顶层还能用 errors.Is 或 __cause__ 判断。
不要这么做 把错误拼成一句字符串再抛(丢了类型,顶层没法判断);用 recover() 兜住之后返回零值(失败看起来像成功);把 err 写进日志就当作处理完(调用方仍不知道)。

工具查不到的边界:进程启动阶段(读配置、连数据库、开监听端口)失败时,可以直接 log.Fatal 退出,这时没有调用方可交还。这个例外只覆盖 main 里装配的那几十行,不覆盖 internal/ 下的任何一行。

助手交出来的样子

rows, _ := db.Query("SELECT id, title FROM notes")   ← 查询失败被丢掉
defer resp.Body.Close()                               ← 关闭失败被丢掉
go func() { report(ctx, note) }()                     ← 没人等它,出错没人知道

认它的办法:把这段代码里所有失败路径列一遍,每一条是不是都有人负责。三个下划线各吞掉一条失败路径:查询失败时 rows 是空的、关闭失败无人知晓、那个 goroutine 出错时没有任何人接。跑起来不报错、测试也能过,代价只在你试图查一次线上问题的时候才出现。

慎用清单,写清代价

下面这些不属于「禁用」,但它们有个共同副作用:助手模仿现有写法时会把它们扩散到不该用的地方。用的时候,在改动说明里写一句为什么这里需要它。

特性代价什么时候可以用
反射与动态字段访问改名后编译不报错,只在运行时出问题只在序列化的边界层用,且该层有测试覆盖字段名
泛型与抽象接口调用点少于两个时,读代码要先跳一层同一组约束在三个以上调用点出现时再抽
并发(goroutine、async)竞态难复现,日志顺序与实际顺序不同有明确的等待或独立任务,且每条路径都配了取消或超时
装饰器与宏改变函数签名跳转到定义看到的形参不是真的形参只用于日志、计时这类横切,不用于改参数语义
any、dynamic、interface{}类型检查失效,字段名拼错也能过只在解析外部输入那一层用,解析完立刻转成具体类型
嵌套三元与链式一行表达式调试时断点落不到中间结果不超过一层;超了就拆成两句

工具查不到的边界:并发那一行在课程项目里最常见的误用是「为了让接口看起来快一点」而开 goroutine,实际没有等待。测试通过只是因为本地执行时间短,没真正走到并发路径。

按错误类型决定级别

同一个函数里要区分「预期内的失败」和「不该发生的失败」:前者记 Info(例如查询没命中),后者记 Error。级别的完整判据与字段清单在日志那一页,本节只给写法。

internal/notes/load.go

用 errors.Is 判断哨兵错误,不要比较错误字符串——字符串一比,包装过的错误就判断不出来了。

if errors.Is(err, notes.ErrNotFound) {
	slog.Info("note not found", "note_id", id)
	return nil
}
slog.Error("load note failed", "note_id", id, "err", err)
return err

工具查不到的边界:只有一种失败路径的函数不必区分,直接返回错误、由上层记一行就够。为每个 if err != nil 各记一条日志,一次请求会打出十几行,检索时被稀释。

必须死守的三条

可预期的失败交还,不中断控制流判据:跑 golangci-lint run 与 npm run lint,退出码为 0;internal/ 与 src/ 下搜不到 panic(、unwrap()、裸 except:。违反后果:一次输入错误让整进程退出,同机其他请求一起失败。
错误包装保留原始错误判据:搜 fmt.Errorf 的每一处都带 %w;Python 里重新抛出时带 from err。违反后果:顶层没法用 errors.Is 判断,只能比字符串,接口状态码跟着错。
不为了通过检查而删掉失败路径判据:改动差异里删掉 if err != nil 却新增业务逻辑的,评审时一律挡回。违反后果:与质量门禁里「不许改判据」是同一类问题,只是发生在代码里而不是配置里。

工具查不到的边界:第一条里的「搜不到 panic(」在 main 包与测试文件里放宽。测试里用 t.Fatal 或 panic 提前结束都是正常的。