应同时检查 ast.assignstmt(tok==token.define)和 ast.declstmt(内含ast.gendecl且tok==token.var),才能完整捕获短变量声明(x := 1)和显式声明(var x int),避免仅过滤ast.gendecl导致遗漏。

用 ast.Inspect 遍历 AST 并识别 *ast.AssignStmt 和 *ast.DeclStmt
Go 的 AST 中,变量声明不只有一种节点类型。常见的是 var x int 这类显式声明(对应 *ast.DeclStmt),但 x := 1 是短变量声明,属于 *ast.AssignStmt 且其 Lhs 全是 *ast.Ident、Rhs 非空。直接过滤 *ast.GenDecl 只能抓到 var/const/type 块,会漏掉 :=。
实操建议:
- 用
ast.Inspect深度遍历,对每个节点做类型断言 - 检查
*ast.AssignStmt:若stmt.Tok == token.DEFINE,则它是短变量声明 - 检查
*ast.DeclStmt:再断言其Decl是否为*ast.GenDecl,且Decl.Tok == token.VAR - 避免只查
*ast.GenDecl—— 它可能在函数外、也可能嵌套在if或for内部,但ast.Inspect能自然覆盖所有作用域
提取变量名和类型时注意 *ast.Ident 和 *ast.TypeSpec 的位置关系
在 *ast.GenDecl 中,变量名在 Specs 的 *ast.ValueSpec 里,而类型信息可能显式写出(如 var x int),也可能省略(如 var x = 1)。短变量声明 x := 1 则完全不带类型语法节点,类型需靠后续类型推导(不在 AST 层)。
实操建议:
-
*ast.ValueSpec.Names是[]*ast.Ident,取Ident.Name即变量名 -
*ast.ValueSpec.Type是类型表达式节点(如*ast.Ident表示基础类型,*ast.StarExpr表示指针),为nil表示类型由右值推导 - 不要尝试从
*ast.AssignStmt的Rhs反推类型——AST 不含类型信息,那是go/types的职责 - 若只需变量名+是否带类型声明,
Type != nil就足够判断
别忘了作用域:同一变量名在不同 block 中是不同声明
AST 本身不记录作用域链或变量是否重复声明,ast.Inspect 返回的节点顺序就是源码出现顺序,但 x := 1 在 if 块内和函数开头是两个独立 *ast.AssignStmt。如果目标是收集“所有局部变量声明”,必须自己维护块层级;如果只是提取所有 var 和 := 出现位置,那直接收就行。
实操建议:
- 用一个整数栈记录进入/退出 block(如遇到
*ast.IfStmt、*ast.ForStmt、*ast.BlockStmt时 push/pop) - 对每个匹配的声明节点,附带当前深度,方便后续区分全局/局部
- 不用解析
go/types.Info——它需要完整构建 type checker,开销大且非纯 AST 任务所需 - 如果只是生成文档或静态检查简单规则,忽略作用域也够用;但做变量重定义检测,就必须跟踪 block
常见错误:把 *ast.Field 或 *ast.StructType 当成变量声明
结构体字段、接收者参数、函数参数都含 *ast.Ident,但它们不是变量声明。比如 func f(x int) 中的 x 是参数,节点是 *ast.Field;type T struct { x int } 中的 x 是字段,也不是局部变量。
实操建议:
- 只响应
*ast.AssignStmt(Tok == DEFINE)和*ast.DeclStmt(内部是*ast.GenDecl且Tok == VAR) - 跳过
*ast.Field、*ast.FuncType、*ast.InterfaceType等上下文中的*ast.Ident - 用
fmt.Printf("%#v", node)打印可疑节点,比猜更快定位真实类型 - 测试时故意写
var _ = 1和_ := 2,确认下划线也被正确捕获——它仍是变量声明,只是被忽略
真正难的不是找到声明节点,而是判断哪些“看起来像变量”的标识符其实不是:接收者名、结构体字段、常量名、甚至 import 别名。只要严格按 AST 节点类型和位置筛选,就不太会错;但一旦想靠名字字符串去 heuristic 匹配,基本就踩进坑了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











