必须用 *token.FileSet 解析 ast.Node 的 token.Pos 才能获得行号,否则 Pos() 返回的只是字节偏移值;Position() 返回的 token.Position.Line 字段才是从1开始的正确行号。

用 ast.Node 配合 token.FileSet 才能拿到行号
Go 的 AST 节点本身不存位置信息,ast.Node 只是语法结构的抽象,真正记录行号、列号的是 token.Pos 类型的位置标记。它必须通过 token.FileSet 解析才能转成文件名、行号、列号。没传或丢了 *token.FileSet,就只能看到一串无意义的整数 pos 值。
常见错误是直接调 node.Pos() 然后打印,结果输出类似 2451 这种数字——这根本不是行号,只是文件内字节偏移的编码值。
- 解析源码时必须显式创建
*token.FileSet,并传给parser.ParseFile - 所有后续对
node.Pos()或node.End()的行号提取,都得用同一个*token.FileSet实例调Position() - 别在多个 goroutine 里共享
*token.FileSet并并发调AddFile,它不是并发安全的(但只读Position是安全的)
fileSet.Position(pos) 返回的 token.Position 才含 Line
token.Position 是唯一暴露行号的结构体,字段 Line 就是你要的行号(从 1 开始),Column 是列号(也是从 1),Filename 是绝对路径(若解析时传了文件名)。
注意:同一份源码被多次 ParseFile 且用了不同 *token.FileSet,得到的行号可能不一致——因为 FileSet 内部靠文件内容哈希或插入顺序管理文件索引,不是靠文件路径自动 dedup。
立即学习“go语言免费学习笔记(深入)”;
fs := token.NewFileSet()
f, _ := parser.ParseFile(fs, "main.go", src, 0)
// 拿 func main 的起始行
if f.Decls != nil {
if fn, ok := f.Decls[0].(*ast.FuncDecl); ok {
pos := fs.Position(fn.Pos())
fmt.Printf("func main starts at line %d\n", pos.Line) // ✅ 正确
}
}遍历 AST 时别漏掉 ast.CommentGroup 的位置
注释节点(*ast.CommentGroup)也实现了 ast.Node 接口,有 Pos() 和 End(),但容易被忽略。如果你在找某段代码“上方最近的注释”,只遍历 Stmt 或 Expr 会跳过它们。
另外,ast.CommentGroup 的 Pos() 指向第一个 /* */ 或 // 的起始位置,不是注释内容开头——比如 // hello 的 Pos() 是 // 两个斜杠的位置,列号为 1。
- 用
ast.Inspect遍历时,要显式检查node == nil后再类型断言,因为CommentGroup可能出现在ast.File.Comments或各节点的Doc/Comment字段 - 想定位某行代码的注释,不能只比对
Line,还要看pos.Offset是否落在该行字节范围内(fileSet.File(pos).LineStart(line)可辅助)
跨文件分析时,fileSet.File(pos) 能帮你确认是否属于目标文件
大型项目常一次解析多个文件,token.FileSet 里会注册多个 *token.File。如果没做校验,直接对任意 node.Pos() 调 fileSet.Position(),可能返回其他文件的行号——尤其当你只关心 main.go 里的节点时。
安全做法是先用 fileSet.File(pos) 拿到对应文件对象,再比对 file.Name() 或用 file.Base() 判断是否匹配目标路径。
targetFile := "main.go"
f := fileSet.File(node.Pos())
if f != nil && filepath.Base(f.Name()) == targetFile {
pos := fileSet.Position(node.Pos())
fmt.Printf("%s:%d", targetFile, pos.Line)
}行号本身不难取,难的是时刻意识到:位置信息是外部依赖,不是节点固有属性;一旦 FileSet 不一致、丢失或复用错,行号就不可信。


















