应优先使用 ast.inspect 而非手写递归,因其自动覆盖所有节点类型、跳过 nil 字段、支持中途退出,且兼容 go 版本升级新增节点(如 typeparams)。

直接用 ast.Inspect,别手写递归遍历 —— 它自动覆盖所有节点类型、跳过 nil 字段、支持中途退出,且 Go 版本升级后不会因新增节点(如 ast.TypeSpec.TypeParams)而漏处理。
为什么 ast.Inspect 比手写 switch node.(type) 更稳
手写类型断言容易漏掉新版本引入的节点类型,比如 Go 1.18 加入的 ast.TypeSpec.TypeParams 或 Go 1.21 的 ast.FieldList 新字段;ast.Inspect 底层遍历的是所有实现了 ast.Node 接口的结构体,无需你手动枚举。
- 回调函数签名固定为
func(node ast.Node) bool,返回false可跳过整棵子树(例如跳过*ast.FuncDecl中的Body) - 不保证访问顺序,也不保证同一节点只出现一次(
*ast.Ident在表达式和类型中可能重复出现),逻辑必须幂等 - 无法获取父节点或作用域上下文 —— 需要自己维护栈,或改用
ast.Walk+ 自定义Visitor - 切勿在回调里修改
node字段:AST 是只读副本,改了不影响原始结构,还可能干扰后续遍历
ast.Inspect 回调里怎么安全取函数名和参数
从 *ast.FuncDecl 提取信息时,Type.Params 和 Type.Results 都可能为 nil,不是空切片,直接 .List 会 panic。
- 函数名在
f.Name.Name,但init函数的Name.Name == "init",没有显式标识,需靠位置或上下文区分 - 判断是否为函数声明:
if f, ok := node.(*ast.FuncDecl); ok { ... },别用==做指针比较(AST 节点没实现==) - 取参数列表前必须判空:
if f.Type.Params != nil { for _, field := range f.Type.Params.List { ... } } - 参数类型可能是
*ast.Ellipsis(对应...T),不是*ast.ArrayType,需单独断言
解析文件时 parser.ParseFile 容易踩的坑
没设对 Mode 参数,90% 的问题就出在这儿 —— 不是 AST 结构看不懂,而是关键信息根本没加载进来。
- 要读注释,必须传
parser.ParseComments,否则file.Comments为空 - 想收集全部语法错误(而非遇到第一个就 panic),得加
parser.AllErrors - 第四个参数传
nil表示从文件路径加载;若要内存解析(比如从字符串读),需传源码内容 +parser.FromFile模式 -
token.FileSet必须传给ParseFile,否则所有位置信息(Pos,End)都是 0,fset.Position(pos)拿不到行号
大型项目需要多次遍历?试试 golang.org/x/tools/go/ast/inspector
标准 ast.Inspect 每次都做完整 DFS,反复解析同一棵树;inspector 预构建事件列表,后续遍历直接扫描,实测快 2.5 倍,适合 callgraph、死代码检测等多轮分析场景。
- 初始化:
in := inspector.New([]*ast.File{file1, file2}) - 只遍历函数声明:
in.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) { ... }) -
Preorder最快,Nodes支持剪枝和后序回调,WithStack自动维护父节点栈 —— 选哪个看是否需要上下文 - 它不替代
ast.Inspect,而是补充:单次轻量分析用Inspect,高频/多策略分析上inspector
真正麻烦的从来不是“怎么遍历”,而是“怎么判断当前节点在哪一层作用域”“怎么把 field.Type 还原成用户可读的类型名”“怎么跨文件关联引用”——这些已经超出 go/ast 范畴,得接 go/types 或 golang.org/x/tools/go/packages 才行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











