inspector比ast.walk更实用,因其自动处理父子关系、跳过注释/空白、支持按类型批量匹配,且能安全预览节点;而手写walk易漏顶层字段(如comments)、误入嵌套节点、难以复用逻辑。

为什么 inspector 比直接遍历 ast.Walk 更实用
因为 inspector 自动处理节点父子关系、跳过注释/空白、支持按类型批量匹配,还能在不破坏 AST 结构的前提下安全地“看一眼就走”。而手写 ast.Walk 容易漏掉 ast.File 顶层字段(比如 Comments)、误入隐式生成节点(如 ast.GenDecl 中的 ast.ValueSpec 嵌套),且难以复用匹配逻辑。
典型使用场景:写 linter 规则、提取函数签名、统计 defer 出现位置、检查未使用的变量声明。
-
inspector的Nodes()方法返回的是可变长切片,不是迭代器——别试图在循环里修改它 - 它不自动进入
ast.CommentGroup,需显式调用inspector.WithStack()并检查ast.CommentGroup类型才能拿到注释内容 - 匹配顺序是深度优先,但节点列表本身是按源码顺序排列的,这点和
ast.Inspect一致
如何正确初始化 *inspector.Inspector 并传入已解析的文件
你不能直接 new 一个 inspector.Inspector;它必须由 inspector.New([]ast.Node) 构造,且参数必须是 *ast.File 切片(不是单个 *ast.File)。常见错误是传入 []ast.Node{f} 却忘了取地址,或把 parser.ParseFile 返回的 *ast.File 直接当 ast.Node 传进去导致 panic。
正确做法:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
files, err := parser.ParseDir(fset, dirPath, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
var astFiles []ast.Node
for _, f := range files {
astFiles = append(astFiles, f)
}
insp := inspector.New(astFiles)
-
fset必须和parser.ParseDir/ParseFile使用同一个*token.FileSet,否则insp.Nodes()返回的位置信息会错乱 - 若只分析单个文件,仍需构造
[]ast.Node{file},不能传file本身 - 若用
go/packages加载代码,应从pkg.Syntax取*ast.File列表,而非pkg.TypesInfo
怎样用 insp.Nodes() 精准匹配特定节点类型(比如所有 ast.CallExpr)
insp.Nodes() 接收一个类型断言切片,内部做的是 reflect.TypeOf(node).Name() == typeName 级别的匹配,不支持接口(如 ast.Expr)或嵌套结构体字段筛选。想抓所有函数调用,就得明确写 []ast.Node{(*ast.CallExpr)(nil)}。
示例:找出所有形如 log.Printf(...) 的调用
var calls []*ast.CallExpr
insp.Nodes([]ast.Node{(*ast.CallExpr)(nil)}, func(n ast.Node) {
call := n.(*ast.CallExpr)
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || !isLogPrintf(sel) {
return
}
calls = append(calls, call)
})
- 匹配列表中每个元素必须是指向具体类型的空指针,
(*ast.CallExpr)(nil)合法,ast.CallExpr{}或nil都会 panic - 回调函数内收到的
n是原始节点指针,可直接类型断言,但不要在回调里修改 AST(如重写call.Args),inspector不保证线程安全 - 若同时匹配多种类型(如
CallExpr和AssignStmt),需分开两次调用Nodes(),它不支持 OR 逻辑
为什么改了代码却没触发 insp.Nodes() 回调?常见漏点
最常被忽略的是:你匹配的节点根本不在当前文件的 AST 树上。比如想查 fmt.Println 调用,但该文件没 import "fmt",或者调用被条件编译(//go:build ignore)排除了;又或者你传入的是测试文件(_test.go),但 parser.ParseDir 默认跳过了它们。
- 检查
parser.ParseDir的 mode 参数是否包含parser.ParseComments(否则注释相关节点为空)和parser.AllErrors(避免静默失败) - 确认目标节点确实存在于 AST 中:用
go/ast.Print(fset, file)打印整棵树,肉眼搜索关键词 -
inspector不处理go/types信息,所以无法区分len([]int{})和len("hello")的参数类型——这类需求得结合types.Info - 如果你在
go/packages.Load后使用inspector,注意pkg.Syntax可能为 nil(比如包加载失败但没报错),要先判空
AST 分析真正难的不是写匹配逻辑,而是确认你看到的树,就是编译器实际看到的那棵——路径、构建标签、go.mod 版本、甚至 GOPROXY 都可能让两棵树长得不一样。










