Go 1.28 vet 新增 scannererr / sqlrowserr:Scan 与 Next 循环里的错误不能再默认忽略
Go 1.28 的 vet 增加了两个分析器(见 tip.golang.org/doc/next 草案):
scannererr:检查bufio.Scanner在Scan循环之后没有处理 scanner 错误,扫描错误或底层 I/O 错误会被漏报。sqlrowserr:对sql.Rows.Next循环做同类检查,让迭代错误与「结果本身就少」区分开。
两者对应的是同一类 bug:循环正常退出并不代表读取过程没有出错。
scannererr 报什么
触发形式:
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
line := sc.Text()
use(line)
}
// 没有 sc.Err()
诊断信息形如:
bufio.Scanner "sc" is used in Scan loop at line N without final check of sc.Err()
实现要点:从 bufio.NewScanner 调用出发,检查返回变量是否在循环中被用于调用 Scan,但从未调用 Err()。
如果 NewScanner 的 reader 参数是「不会失败」的类型——strings.Reader、bytes.Reader、bytes.Buffer——分析器会跳过,避免噪音。这些数据源虽然也可能在 token / 行超过内部缓冲时失败,但远不如真实 I/O 常见。
sqlrowserr 对应 Rows.Next
Rows.Next 在遇到错误时同样返回 false,和「结果集读完」表现一致。只写循环不写 Rows.Err(),错误就被当成结果更少:
rows, err := db.Query(q)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return err
}
use(name)
}
// 没有 rows.Err()
背景:issue #17747 到 22K 模块实测
相关讨论在 golang/go#17747,实现提交为 golang/tools commit 85bb374。
- 有团队反馈,代码库里漏检
Scanner.Err/Rows.Err的用法约占 10%; - 在约 100 处
Scan/Next调用里,事后补上Err检查全部是正确的; - 分析器在 22K 个模块上跑出 2337 处命中,来自 1162 个模块;
- 把「无失败可能的 reader」排除之后,噪音大幅下降。
落地上,分析器先进入 gopls(CL 730480),再纳入 cmd/vet——安排在 Go 1.27 发布完成之后,实际作为 Go 1.28 vet 的新增项。
正确写法
sc := bufio.NewScanner(f)
for sc.Scan() {
// ...
}
if err := sc.Err(); err != nil {
return err
}
rows, err := db.Query(q)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// ...
}
if err := rows.Err(); err != nil {
return err
}
共同点是把 Err() 放在循环之外、退出循环之后立刻检查,而不是在循环体内判断。
落地建议
gopls 侧:分析器先进 gopls,编辑器里打开对应诊断即可第一时间看到报告,不需要等 go vet 落地。
go vet 侧:Go 1.28 起随 cmd/vet 提供,go test 默认会跑 vet,CI 上无需额外配置。存量代码按 Scan / Next 调用点做一轮审计,重点看读取文件、stdin、网络连接、数据库结果集的位置——这些才是真正会出错的路径。