Go scanner.Scanner 自定义 Error 回调怎样保留文件行列号
来源:17golang原创
时间:2026-11-05 09:06:08 107浏览 收藏
使用 go/scanner 扫描 Go 源码时,自定义错误处理应通过 Scanner.Init 的 ErrorHandler 参数注册。回调已经给出 token.Position 和消息,保留整份 Position,就能保存文件名、行号、字节列与字节偏移,展示时再格式化。
先澄清标题里的 Error:go/scanner.Scanner 没有可赋值的 Error 字段,text/scanner.Scanner 才有同名字段。本文围绕 go/scanner 的错误回调,不把另一个包的写法混进来,也不扩展到 parser 的错误恢复。
官方地址:https://pkg.go.dev/go/scanner#Scanner
只有错误消息时,先确认导入的是哪个 scanner
设想一个源码检查工具能显示“非法转义”,却无法跳到文件中的具体位置。我会先看收集器存了什么:如果回调只把 msg 追加到字符串切片,行列号往往不是扫描器没有提供,而是在应用封装时被丢掉了。
第二步看导入路径。go/scanner 与 text/scanner 的包名都叫 scanner,复制片段时容易只看到类型名。前者以源字节和 token.File 初始化,错误处理是 Init 的一个参数;后者的初始化和 Error 字段采用另一套接口。两者不能只改 import 就直接互换。
对这个问题,我更愿意先把错误记录结构定义完整,再考虑日志格式。文本输出是最后一层,位置一旦提前压成一段消息,后续排序、定位和交给编辑器都会更加麻烦。
在 Init 回调里保留完整 token.Position

文件名来自传给 FileSet.AddFile 的 name。扫描器处理的是内存中的 src,不会替你从磁盘推断文件路径。给 name 传空字符串,即使行列有效,位置文本也不包含文件名。
AddFile 的 size 应为 len(src),即实际扫描字节长度,不能用字符数代替。Scanner.Init 要求 token.File 的大小和源字节长度匹配。本文用 base=-1 让文件集合选择可用区间,不手工拼接位置编号。
回调的 pos 已经是展开后的 token.Position,无需把它强转成 token.Pos 再查询文件集合。扫描 token 时返回的紧凑位置才需要通过对应 FileSet 展开。这两类位置对象的用途要在命名中区分。
用一份错误样本同时保存位置与消息
下面是一份原创完整示例。输入里的字符串含非法转义,用于触发词法诊断。代码把位置和消息一起收集,扫描到 EOF 后逐条展示,并另外给出 ErrorCount。这里只给出使用方式,没有执行示例或虚构运行输出。
package main
import (
"fmt"
"go/scanner"
"go/token"
)
type diagnostic struct {
Position token.Position
Message string
}
func scanSource(name string, src []byte) ([]diagnostic, int) {
fset := token.NewFileSet()
file := fset.AddFile(name, -1, len(src))
var issues []diagnostic
var s scanner.Scanner
// Init 也可能报告首字符错误,收集器必须提前准备。
handler := func(pos token.Position, msg string) {
issues = append(issues, diagnostic{
Position: pos, // 按值保存完整位置,不只保存消息。
Message: msg,
})
}
s.Init(file, src, handler, 0)
for {
_, tok, _ := s.Scan()
if tok == token.EOF {
break
}
// 错误依据来自回调和 ErrorCount,不只判断 ILLEGAL。
}
return issues, s.ErrorCount
}
func main() {
// 用非法转义构造词法错误,不涉及 parser 的语法恢复。
src := []byte("package sample\nvar name = \"\\q\"\n")
issues, count := scanSource("sample.go", src)
for _, issue := range issues {
p := issue.Position
fmt.Printf("%s: %s [offset=%d]\n", p.String(), issue.Message, p.Offset)
}
fmt.Printf("扫描错误数: %d\n", count)
}
Position 是按值复制的记录。上层展示可以使用 Position.String() 得到适合阅读的文件行列形式,也可以分别读取 Filename、Line、Column。Offset 则保留给需要关联原始源字节的调用方。
handler 在 Init 之前就准备好,因为初始化可能在检查首字符时调用错误处理器。若把诊断切片的重置放到 Init 之后,就可能把首字符错误覆盖掉;若只观察后续 Scan 的返回值,也会漏掉这种初始化阶段的诊断。
示例的 src 是一份固定字节内容。实际工具还应把它关联到明确的源文件版本,避免诊断记录产生后用户编辑了文件,却仍按旧偏移跳转。这个版本关联由应用管理,不是扫描器自动替你保存的文件快照。
错误位置与 Scan 返回位置分别处理
Scan 返回位置、token 和字面量;错误回调另行提供错误位置与消息。这两条信息通道不应互相替代。如果回调闭包去读取外层“上一次 Scan 的 pos”,拿到的可能是上一个 token,而不是当前错误的位置。
我会让错误记录只采用回调直接传入的位置,token 列表则采用每次 Scan 返回的位置。即使二者偶尔对应同一个源片段,也不把这种巧合写成通用规则。
扫描器还可能在遇到错误时返回可识别的 token。因此没有 token.ILLEGAL,并不能证明输入完全没有扫描错误。对于本文的收集器,issues 与 ErrorCount 才是诊断依据;如果应用自行过滤部分消息,也要明确过滤后的条数与原始错误数可能不同。
| 信息来源 | 适合保存 | 避免的替代写法 |
|---|---|---|
| ErrorHandler 的 pos | 对应诊断的位置 | 借用外层上一次 token 的位置 |
| Scan 返回的 token.Pos | token 在文件集合中的紧凑位置 | 把整数直接当行号或字节偏移 |
| ErrorCount | 扫描器发现的错误数量 | 只数 ILLEGAL token |
| msg | 错误描述文本 | 只存描述而丢掉结构化位置 |
行列仍然对不上时,检查字节与位置映射

token.Position 的 Offset 从零开始,Line 从一开始,Column 也是从一开始,但按字节计数。含中文等 UTF-8 多字节内容时,字节列不等于屏幕上看到的字符列;制表符的显示宽度也会受到编辑器设置影响。
因此我会原样保留标准库给出的 Column,展示层若要画光标,再基于同一份源内容做坐标转换。不要直接减去某个固定数字,也不要把 token 的全局 Pos 值当成本文件 Offset。
还需注意位置调整指令。源码中的 //line 注释可能影响展示文件名和行列含义。拿到 Scan 返回的 token.Pos 时,可用 FileSet.PositionFor 的 adjusted 参数选择是否考虑调整;错误回调提供的是展开后的 Position,原始物理位置需求应明确记录,并基于注册的 token.File 与有效 Offset 另行映射。
普通源码诊断通常直接保留回调位置即可。处理生成代码时,建议把“给用户看的逻辑位置”和“源字节中的物理位置”分开存储,避免一个字段在不同阶段被覆盖成另一种含义。
把诊断交给上层前保留哪些信息
如果暂时不需要自定义结构体,标准库的 ErrorList 也能保存位置和消息。下面是替代收集方式的局部片段,沿用已有的 file、src 及导入;它不是需要接在前面循环后再重复扫描的步骤。
// 这个片段使用已有的 file、src;错误列表同时保存位置和消息。
var issues scanner.ErrorList
var s scanner.Scanner
s.Init(file, src, issues.Add, 0)
for {
_, tok, _ := s.Scan()
if tok == token.EOF {
break
}
}
// 逐条输出,而不是把整份列表压成一个摘要字符串。
for _, issue := range issues {
fmt.Println(issue.Error())
}
我会保留每条错误再交给上层,而不是过早调用聚合的 Error 字符串当作全部数据。需要排序时可以使用列表提供的方法;需要去重时则先定义业务规则,不要在还没定位“行列为什么消失”之前就删掉记录。
- 确认导入 go/scanner,并通过 Init 注册回调。
- 文件名与源字节长度在 AddFile 时确定。
- 错误位置直接采用回调参数,消息和位置一起保存。
- 扫描完成检查 ErrorCount,不能只看 ILLEGAL。
- 保留字节列与 Offset,展示层另做字符或屏幕坐标转换。
Init 的错误回调传 nil,会自动把详细错误写出来吗?
不要依赖它生成你需要的报告。go/scanner 中应安装自己的处理器来收集详细诊断,并通过 ErrorCount 了解错误数量;这也避免把 text/scanner 的默认打印行为误套过来。
文件名为空,能在打印时补一个吗?
可以在应用展示层提供标签,但更直接的做法是在注册 token.File 时就传入正确名称,并让记录保留来源身份。多文件场景尤其不宜事后根据循环变量猜测文件名。
为什么有错误却仍然能继续拿到 token?
扫描接口会尽可能继续给出可识别 token。继续扫描不等于错误不存在,应保留诊断记录。本文不据此承诺 parser 能恢复全部语法问题。
参考:go/scanner 错误回调与错误列表、go/token 位置字段和映射、text/scanner 接口区别。示例与排查场景为本文原创。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
439 收藏
-
428 收藏
-
147 收藏
-
499 收藏
-
368 收藏
-
484 收藏
-
403 收藏
-
255 收藏
-
424 收藏
-
194 收藏
-
359 收藏
-
126 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习