应优先使用 ast.Inspect 遍历 AST,避免手写递归;parser.ParseFile 必须配对 token.NewFileSet() 和正确 mode,否则易因 fset 为 nil、mode 为 0、src 误用等导致字段为空或 panic。

直接用 ast.Inspect 遍历,别手写递归;parser.ParseFile 必须配对 token.NewFileSet() 和正确的 mode,否则字段为空或 panic 是常态。
parser.ParseFile 调用前必须检查的四件事
它不报错不代表成功,很多 panic 来自调用姿势不对:
-
fset必须非 nil —— 用token.NewFileSet()创建,漏了会导致所有node.Pos()返回0,后续定位失效 - 第二个参数(文件名)可传空字符串
"",但若从磁盘读取,建议传绝对路径;若用于内存解析(比如传入src字符串),它只是占位符,不校验是否存在 - 第四个参数
mode不能为0或nil:要读注释就得加parser.ParseComments;想收集全部语法错误,得加parser.AllErrors -
src参数决定读取方式:传nil表示从文件路径加载;传字符串(如os.ReadFile结果)则必须非 nil,否则解析器直接忽略
遍历 *ast.FuncDecl 时最常 panic 的三个位置
函数声明结构比直觉中更“稀疏”,字段为 nil 是合法且常见的情况:
-
funcDecl.Type.Params和funcDecl.Type.Results都可能为nil(例如func hello() {}或func goodbye() error),不是空切片,直接访问.List必 panic -
funcDecl.Name是*ast.Ident,取名字必须用funcDecl.Name.Name;init函数的Name.Name == "init",没有导出标识,需靠上下文区分 - 参数是
*ast.Field,其Names是[]*ast.Ident,匿名参数(如func(int))该切片为空,取[0]必 panic;类型字段可能是*ast.Ellipsis(对应...T),不是*ast.ArrayType
为什么 ast.Inspect 比手写 switch node.(type) 更稳
不是它更“高级”,而是它绕开了 Go AST 演进带来的维护陷阱:
立即学习“go语言免费学习笔记(深入)”;
- 手写类型断言容易漏掉新版本节点:Go 1.18 加了
ast.TypeSpec.TypeParams,Go 1.21 改了ast.FieldList字段行为,你不会每次升级都去翻源码补case *ast.Xxx -
ast.Inspect底层按ast.Node接口遍历,自动覆盖所有实现该接口的结构体,包括未来版本新增的节点类型 - 它自动跳过 nil 子节点(比如
FuncDecl.Doc为空时不会进入),而手写递归需处处判空,极易遗漏 - 回调返回
false可跳过整棵子树(如跳过*ast.FuncDecl.Body),适合“只找函数签名不看实现”的场景
什么时候该换 inspector 而不是反复用 ast.Inspect
同一组 AST 文件要做多次不同目的遍历(比如先找未使用变量、再统计圈复杂度、再提取注释),ast.Inspect 就成了性能瓶颈:
-
inspector.New([]*ast.File{f1, f2})初始化时一次性构建事件索引,后续遍历直接扫描,实测快 2.5 倍 - 只查函数?传
[]ast.Node{(*ast.FuncDecl)(nil)}给Preorder,避免无谓节点访问 - 需要前后序逻辑(如进函数时计数、出函数时汇总),用
Nodes方法,它比Preorder多一层控制粒度 - 注意:
inspector在 Go 1.21+ 更稳;低于此版本建议 stick withast.Inspect
AST 节点是只读副本,改了字段不影响原始结构;真正要重写代码,得用 golang.org/x/tools/go/ast/astutil 或手动生成新节点。最易被忽略的是:所有位置信息(Pos/End)都依赖同一个 token.FileSet 实例,跨解析复用或并发共享会彻底破坏行号映射。


















