第43章:Go 语法树——go/ast、go/parser

第43章:Go 语法树——go/ast、go/parser

“程序员最浪漫的事,不是写代码,而是让代码自己读懂自己。”

当你写下一个 fmt.Println("Hello"),Go 编译器默默把你的代码拆成一个个最小的语法单元,然后像搭积木一样把它们组装成一棵树。这棵树,就是抽象语法树(AST)。有了它,IDE 能给你自动补全,gofmt 能帮你格式化,静态分析工具能找出你代码里的 bug。这,就是 go/ast 和 go/parser 的魔法世界。


43.1 go/ast 包解决什么问题:词法分析输出 token,语法分析把 token 组织成树

问题引入:代码是怎么被"理解"的?

当你打开一个 Go 文件,编译器看到的是一堆字节。但它需要"理解"这些字节的含义。这就要分两步走:

  1. 词法分析(Lexical Analysis):把源代码字符串拆成一个个 token(词法单元)。比如 age := 18 会被拆成 标识符(age)赋值(:=)数字(18)分号
  2. 语法分析(Syntax Analysis):把这些 token 按照语法规则组装成一棵树——AST(抽象语法树)

Go 官方把这两步分别交给了 go/scanner(词法分析)和 go/parser(语法分析),而 go/ast 则负责定义这棵树的节点类型。

词法分析 vs 语法分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"go/scanner"
	"go/token"
)

func main() {
	// 源代码
	src := []byte(`age := 18`)

	// 创建一个 token 文件集(记录每个 token 的位置)
	fset := token.NewFileSet()

	// 创建一个 scanner
	var s scanner.Scanner
	s.Init(fset.AddFile("main.go", fset.Base(), len(src)), src, nil, scanner.ScanComments)

	// 循环扫描所有 token
	for {
		pos, tok, lit := s.Scan()
		if tok == token.EOF {
			break
		}
		// 打印每个 token:位置、类型、字面量
		fmt.Printf("位置: %s, 类型: %-8s, 字面量: %q\n", fset.Position(pos), tok, lit)
	}
}
位置: main.go:1:1, 类型: IDENT   , 字面量: "age"
位置: main.go:1:5, 类型: :=      , 字面量: ""
位置: main.go:1:8, 类型: INT     , 字面量: "18"
位置: main.go:1:10, 类型: ;      , 字面量: ""

术语解释

术语英文含义
tokenToken词法分析的最小单元,代表源代码中的一个原子元素
词法分析Lexical Analysis将源代码字符串分解为 token 序列的过程
语法分析Syntax Analysis将 token 序列按照语法规则组织成 AST 的过程
ASTAbstract Syntax Tree抽象语法树,以树形结构表示源代码的语法结构

43.2 go/ast 核心原理:Node 接口体系,Expr、Stmt、Decl、Spec

Go AST 的核心接口

go/ast 包定义了一个核心接口 Node,所有 AST 节点都实现了这个接口:

1
2
3
4
5
// Node 接口——所有 AST 节点的"老祖宗"
type Node interface {
	Pos() token.Pos   // 节点在源代码中的起始位置
	End() token.Pos   // 节点在源代码中的结束位置
}

在这个接口之下,Go 将节点分成了四大类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 表达式——有值的东西,比如 1+2、fmt.Println、a[i]
type Expr interface {
    Node
    exprNode()
}

// 语句——执行动作,比如 if、for、赋值
type Stmt interface {
    Node
    stmtNode()
}

// 声明——定义东西,比如 var、type、func
type Decl interface {
    Node
    declNode()
}

// 规范声明的子节点——用于 Spec(规范)中
type Spec interface {
    Node
    specNode()
}

节点类型体系图

graph TD
    A["Node 接口<br/>(所有节点的祖先)"] --> B["Expr 接口<br/>(表达式)"]
    A --> C["Stmt 接口<br/>(语句)"]
    A --> D["Decl 接口<br/>(声明)"]
    
    B --> B1["*ast.Ident<br/>(标识符)"]
    B --> B2["*ast.BasicLit<br/>(字面量)"]
    B --> B3["*ast.BinaryExpr<br/>(二元表达式)"]
    B --> B4["*ast.CallExpr<br/>(函数调用)"]
    B --> B5["*ast.FuncLit<br/>(函数字面量)"]
    B --> B6["*ast.CompositeLit<br/>(复合字面量)"]
    
    C --> C1["*ast.IfStmt<br/>(if语句)"]
    C --> C2["*ast.ForStmt<br/>(for循环)"]
    C --> C3["*ast.AssignStmt<br/>(赋值语句)"]
    C --> C4["*ast.ExprStmt<br/>(表达式语句)"]
    C --> C5["*ast.RangeStmt<br/>(range语句)"]
    
    D --> D1["*ast.FuncDecl<br/>(函数声明)"]
    D --> D2["*ast.GenDecl<br/>(通用声明:var/const/type)"]
    D --> D3["*ast.ImportSpec<br/>(导入声明)"]
    
    B --> E["Spec 接口<br/>(规范子节点)"]
    E --> E1["*ast.ValueSpec<br/>(变量规范)"]
    E --> E2["*ast.TypeSpec<br/>(类型规范)"]
    E --> E3["*ast.ImportSpec<br/>(导入规范)"]

为什么分这么多接口?

这其实是一种**组合模式(Composite Pattern)**的应用。通过接口分层,Go 的 ast.Inspectast.Walk 等工具可以用统一的方式遍历不同类型的节点——它们只需要判断一个 Node 是否实现了 ExprStmtDecl,就能知道该如何处理它。

术语解释

术语英文含义
ExprExpression表达式,有值,可以求值
StmtStatement语句,执行动作,不返回值
DeclDeclaration声明,定义一个名称
SpecSpecification规范声明的子节点,如 ValueSpec、TypeSpec

43.3 ast.File:完整文件的 AST 表示

什么是 ast.File?

当你用 parser.ParseFile 解析一个 .go 文件时,得到的就是一个 *ast.File。它代表一个完整的 Go 源代码文件,包含了这个文件所有的组成部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ast.File 的结构(简化版)
type File struct {
    Doc     *CommentGroup   // 包文档注释
    Package token.Pos       // package 关键字的位置
    Name    *Ident          // 包名(如 main、fmt)
    Decls   []Decl          // 文件中的所有声明(import、type、var、func...)
    Scope   *Scope          // 文件级作用域
    Imports []*ImportSpec   // 所有导入的包
    Comments []*CommentGroup // 所有注释
}

解析并查看文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	// 创建一个 token 文件集
	fset := token.NewFileSet()

	// 解析一个 Go 源代码字符串
	src := `package main

import "fmt"

// Add 计算两数之和
func Add(a, b int) int {
	return a + b
}

func main() {
	fmt.Println(Add(1, 2))
}`

	// 解析源代码,返回 *ast.File
	f, err := parser.ParseFile(fset, "main.go", src, parser.ParseComments)
	if err != nil {
		panic(err)
	}

	// 打印文件基本信息
	fmt.Printf("包名: %s\n", f.Name.Name)
	fmt.Printf("包文档: %s\n", f.Doc.Text())
	fmt.Printf("导入包数量: %d\n", len(f.Imports))
	for _, imp := range f.Imports {
		fmt.Printf("  - 导入路径: %s\n", imp.Path.Value)
	}

	// 遍历所有声明
	fmt.Printf("声明数量: %d\n", len(f.Decls))
	for i, decl := range f.Decls {
		fmt.Printf("  [%d] 类型: %T\n", i, decl)
	}
}
包名: main
包文档: 
导入包数量: 1
  - 导入路径: "fmt"
声明数量: 3
  [0] 类型: *ast.GenDecl
  [1] 类型: *ast.FuncDecl
  [2] 类型: *ast.FuncDecl

ast.File 在工具链中的地位

graph LR
    A["源代码<br/>main.go"] --> B["parser.ParseFile"]
    B --> C["*ast.File"]
    C --> D["go/ast 节点树"]
    C --> E["go/printer 打印"]
    C --> F["gofmt 格式化"]
    C --> G["静态分析"]

ast.File 是整个 go/ast 包的门面,所有对文件级别操作都离不开它。


43.4 ast.Package:包(多个文件)

什么是 ast.Package?

当你的代码分布在多个 .go 文件中(比如 a.gob.go),它们共同组成一个 Packageast.Package 就是对这种多文件包的抽象——它把同一个包名下所有的 ast.File 汇总在一起。

1
2
3
4
5
6
7
// ast.Package 的结构(简化版)
type Package struct {
    Name    string             // 包名
    Scope   *Scope             // 包级作用域
    Imports map[string]*File   // 文件名到 ast.File 的映射
    Files   map[string]*File   // 文件路径到 ast.File 的映射(新版 Go 更喜欢用 Files)
}

解析整个包目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"go/parser"
	"go/token"
)

func main() {
	fset := token.NewFileSet()

	// 解析一个目录(假设当前目录有一些 .go 文件)
	// 这里用 "" 表示解析内置的测试用包
	// 实际使用时:parser.ParseDir(fset, "/path/to/package", nil, 0)
	pkg, err := parser.ParseDir(fset, ".", func(info os.FileInfo) bool {
		return !info.IsDir() && !strings.HasSuffix(info.Name(), "_test.go")
	}, 0)
	if err != nil {
		panic(err)
	}

	// 遍历包中的所有文件
	for name, file := range pkg.Files {
		fmt.Printf("文件: %s, 包名: %s, 声明数: %d\n",
			name, file.Name.Name, len(file.Decls))
	}
}

注意parser.ParseDir 的第二个参数是包所在的目录路径,上面的代码需要在真实目录下运行才能看到结果。

ast.Package vs ast.File

graph TD
    A["ast.Package<br/>(一个包,多个文件)"] --> B["*ast.File<br/>a.go"]
    A --> C["*ast.File<br/>b.go"]
    A --> D["*ast.File<br/>c.go"]
    
    B --> E["Decls: [import, func A, func B]"]
    C --> F["Decls: [func C, type T]"]
    D --> G["Decls: [var x int, func main]"]

简单来说:

  • ast.File = 一个 .go 文件的 AST
  • ast.Package = 同一个包名下所有 .go 文件的 AST 集合

43.5 ast.Ident:标识符

什么是 ast.Ident?

标识符(Identifier)就是源代码中的名字——变量名、函数名、包名、类型名,统统都是 *ast.Ident

1
2
3
4
5
6
// ast.Ident 的结构
type Ident struct {
	NamePos token.Pos  // 名字出现的位置
	Name    string     // 标识符的名称
	Obj     *Object    // 指向作用域中对应对象的引用(解析后填充)
}

用处多多的标识符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

func greet(name string) {
	msg := "Hello, " + name
	println(msg)
}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	// 使用 ast.Inspect 遍历所有节点,找所有 Ident
	fmt.Println("找到的标识符:")
	ast.Inspect(f, func(n ast.Node) bool {
		if ident, ok := n.(*ast.Ident); ok {
			fmt.Printf("  名字: %-10s 位置: %s\n", ident.Name, fset.Position(ident.Pos()))
		}
		return true // 返回 true 继续遍历
	})
}
找到的标识符:
  名字: main      位置: main.go:2:10
  名字: greet     位置: main.go:3:6
  名字: string    位置: main.go:3:12
  名字: name      位置: main.go:3:17
  名字: msg       位置: main.go:5:3
  名字: string    位置: main.go:5:15
  名字: name      位置: main.go:6:20
  名字: println   位置: main.go:7:3

有趣的是,string 也被当作 *ast.Ident 识别了——因为 Go 的预定义类型名本质上也是一个标识符。

术语解释

术语英文含义
标识符Identifier用来命名变量、函数、类型等的符号
作用域Scope标识符的有效范围
ObjectObject标识符在作用域中对应的对象,存储其元信息

43.6 ast.BasicLit:基本字面量

什么是 ast.BasicLit?

基本字面量就是那些"字面"的值——数字 42、字符串 "hello"、字符 'a'、布尔值 true/false(Go 里布尔没有字面量表示,但常量有)。在 AST 中,这些统一用 *ast.BasicLit 表示。

1
2
3
4
5
6
// ast.BasicLit 的结构
type BasicLit struct {
	ValuePos token.Pos   // 值出现的位置
	Kind     token.Token // 值的类型:INT、FLOAT、IMAG、CHAR、STRING
	Value    string      // 字面值的字符串表示(未经解析的原始字符串)
}

识别各种基本字面量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

const (
	age     = 18
	pi      = 3.14159
	version = "v1.0.0"
	none    = '\u0000'
)`)

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	ast.Inspect(f, func(n ast.Node) bool {
		if lit, ok := n.(*ast.BasicLit); ok {
			fmt.Printf("字面值: %-15s 类型: %-8s 位置: %s\n",
				lit.Value, lit.Kind, fset.Position(lit.Pos()))
		}
		return true
	})
}
字面值: 18           类型: INT     位置: main.go:3:13
字面值: 3.14159      类型: FLOAT   位置: main.go:4:13
字面值: "v1.0.0"     类型: STRING  位置: main.go:5:16
字面值: '\u0000'     类型: CHAR    位置: main.go:6:15

字面量类型对照表

Kind示例说明
INT420xFF1e10整数(包括十进制、十六进制、科学计数法)
FLOAT3.141e-9浮点数
IMAG3.14i虚数(Go 独有)
CHAR'a''\n''\u4e2d'字符(单引号)
STRING"hello""中文"\ raw \字符串

43.7 ast.FuncLit:函数字面量,匿名函数

什么是 ast.FuncLit?

ast.FuncLit 就是匿名函数——没有名字的函数。你可以把它赋值给变量、作为参数传递、作为返回值,简直是函数式编程的瑞士军刀。

1
2
3
4
5
// ast.FuncLit 的结构
type FuncLit struct {
	Type *FuncType  // 函数类型签名(参数和返回值)
	Body *BlockStmt // 函数体(花括号里的代码块)
}

匿名函数在 AST 中的样子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

func main() {
	// 匿名函数赋值给变量
	add := func(a, b int) int {
		return a + b
	}
	fmt.Println(add(1, 2))

	// 匿名函数直接调用
	result := func(x int) int {
		return x * x
	}(5)
	fmt.Println(result)

	// 匿名函数作为参数
	visit([]int{1, 2, 3}, func(n int) {
		fmt.Println(n)
	})
}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	ast.Inspect(f, func(n ast.Node) bool {
		if fl, ok := n.(*ast.FuncLit); ok {
			// 获取所在位置附近的上下文
			pos := fset.Position(fl.Pos())
			fmt.Printf("发现匿名函数!位置: %s\n", pos)
			if fl.Type.Params != nil {
				fmt.Printf("  参数数量: %d\n", len(fl.Type.Params.List))
			}
			fmt.Printf("  有返回值: %v\n", fl.Type.Results != nil)
		}
		return true
	})
}
发现匿名函数!位置: main.go:4:11
  参数数量: 2
  有返回值: true
发现匿名函数!位置: main.go:10:13
  参数数量: 1
  有返回值: true
发现匿名函数!位置: main.go:15:24
  参数数量: 1
  有返回值: false

FuncLit vs FuncDecl 对比

graph TD
    A["FuncDecl<br/>(函数声明)"] --> A1["有名字的函数"]
    A1 --> A2["FuncType + Body"]
    A1 --> A3["Name: *Ident<br/>名字是 Ident"]
    
    B["FuncLit<br/>(函数字面量)"] --> B1["匿名函数"]
    B1 --> B2["FuncType + Body"]
    B1 --> B3["没有 Name 字段<br/>名字在外部通过赋值获得"]
    
    style A fill:#90EE90
    style B fill:#ADD8E6

核心区别:FuncDecl 有名字(Name *Ident),FuncLit 没有名字——它是"字面"存在的函数,像数字 42 一样可以直接使用。


43.8 ast.CompositeLit:复合字面量

什么是 ast.CompositeLit?

复合字面量就是用 T{...} 语法直接构造类型值的写法。比如 []int{1, 2, 3}map[string]int{"a": 1}、结构体 Point{X: 1, Y: 2},都是复合字面量。

1
2
3
4
5
6
7
8
// ast.CompositeLit 的结构
type CompositeLit struct {
	Type       Expr      // 类型表达式(如 *ast.MapType、*ast.StructType,可以为 nil 表示简短形式)
	Lbrace     token.Pos // 左花括号位置
	Elts       []Expr    // 元素列表
	Rbrace     token.Pos // 右花括号位置
	Incomplete bool      // 解析是否不完整
}

各种复合字面量一览

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

type Point struct {
	X int
	Y int
}

func main() {
	// 结构体复合字面量
	p1 := Point{X: 1, Y: 2}
	p2 := Point{1, 2}

	// 切片复合字面量
	nums := []int{1, 2, 3, 4, 5}

	// 数组
	arr := [3]int{10, 20, 30}

	// map
	m := map[string]int{"a": 1, "b": 2}

	// 嵌套
	matrix := [][]int{{1, 2}, {3, 4}}
}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	ast.Inspect(f, func(n ast.Node) bool {
		if cl, ok := n.(*ast.CompositeLit); ok {
			pos := fset.Position(cl.Pos())
			fmt.Printf("复合字面量 @ %s\n", pos)

			// 打印类型
			if cl.Type != nil {
				switch t := cl.Type.(type) {
				case *ast.Ident:
					fmt.Printf("  类型: %s (结构体/自定义类型)\n", t.Name)
				case *ast.ArrayType:
					fmt.Printf("  类型: []%s (数组/切片)\n", typeString(t.Elt))
				case *ast.MapType:
					fmt.Printf("  类型: map[%s]%s\n",
						typeString(t.Key), typeString(t.Value))
				}
			} else {
				fmt.Printf("  类型: 简短形式(从上下文推断)\n")
			}
			fmt.Printf("  元素数量: %d\n", len(cl.Elts))
		}
		return true
	})
}

// 辅助函数:获取类型的字符串表示
func typeString(e ast.Expr) string {
	if ident, ok := e.(*ast.Ident); ok {
		return ident.Name
	}
	return "unknown"
}
复合字面量 @ main.go:9:12
  类型: 简短形式(从上下文推断)
  元素数量: 2
复合字面量 @ main.go:10:12
  类型: 简短形式(从上下文推断)
  元素数量: 2
复合字面量 @ main.go:13:12
  类型: []int (数组/切片)
  元素数量: 5
复合字面量 @ main.go:16:12
  类型: []int (数组/切片)
  元素数量: 3
复合字面量 @ main.go:19:12
  类型: map[string]int
  元素数量: 2
复合字面量 @ main.go:22:12
  类型: 简短形式(从上下文推断)
  元素数量: 2

术语解释

术语英文含义
CompositeLitComposite Literal复合字面量,用 T{...} 语法直接构造值
StructTypeStruct Type结构体类型
ArrayTypeArray Type数组/切片类型
MapTypeMap Type字典类型

43.9 ast.FuncDecl:函数声明

什么是 ast.FuncDecl?

ast.FuncDecl 代表一个命名函数的声明——也就是我们最常见的 func FunctionName() {}

1
2
3
4
5
6
7
// ast.FuncDecl 的结构
type FuncDecl struct {
	Recv *FieldList  // 接收者(如果是方法则为非 nil)
	Name *Ident      // 函数名
	Type *FuncType   // 函数类型签名
	Body *BlockStmt  // 函数体(nil 表示接口方法声明或外部函数)
}

函数声明与 AST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

import "fmt"

// Add 求两数之和(普通函数)
func Add(a, b int) int {
	return a + b
}

// SayHello 打招呼(带接收者的方法)
func (p *Person) SayHello() {
	fmt.Println("Hello!")
}

// Internal 不导出(私有函数)
func internal(x int) int {
	return x * 2
}

// NoBody 没有函数体(外部函数声明,如 syscall/js)
func NoBody()`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	for _, decl := range f.Decls {
		if fd, ok := decl.(*ast.FuncDecl); ok {
			fmt.Printf("函数: %s\n", fd.Name.Name)
			fmt.Printf("  位置: %s\n", fset.Position(fd.Name.Pos()))
			fmt.Printf("  是否导出: %v\n", token.IsExported(fd.Name.Name))
			fmt.Printf("  是方法: %v\n", fd.Recv != nil)
			fmt.Printf("  有函数体: %v\n", fd.Body != nil)

			// 打印签名
			if fd.Type.Params != nil {
				fmt.Printf("  参数数: %d\n", len(fd.Type.Params.List))
			}
			if fd.Type.Results != nil {
				fmt.Printf("  返回值数: %d\n", len(fd.Type.Results.List))
			}
			fmt.Println()
		}
	}
}
函数: Add
  位置: main.go:7:6
  是否导出: true
  是方法: false
  有函数体: true
  参数数: 2
  返回值数: 1

函数: SayHello
  位置: main.go:12:6
  是否导出: true
  是方法: true
  有函数体: true
  参数数: 0
  返回值数: 0

函数: internal
  位置: main.go:18:6
  是否导出: false
  是方法: false
  有函数体: true
  参数数: 1
  返回值数: 1

函数: NoBody
  位置: main.go:23:6
  是否导出: true
  是方法: false
  有函数体: false
  参数数: 0
  返回值数: 0

FuncDecl 结构图

graph LR
    A["*ast.FuncDecl"] --> B["Recv *FieldList<br/>(接收者)"]
    A --> C["Name *Ident<br/>(函数名)"]
    A --> D["Type *FuncType<br/>(签名)"]
    A --> E["Body *BlockStmt<br/>(函数体)"]
    
    D --> D1["Params *FieldList<br/>(参数列表)"]
    D --> D2["Results *FieldList<br/>(返回值列表)"]
    
    B --> B1["List []*Field<br/>(字段列表)"]
    B1 --> B2["Names []*Ident<br/>(参数名)"]
    B1 --> B3["Type ast.Expr<br/>(参数类型)"]

43.10 ast.GenDecl:通用声明,const、type、var

什么是 ast.GenDecl?

ast.GenDecl 是"通用声明"——用来表示 consttypevar 这三种声明。它专门处理批量声明,比如 const (a = 1; b = 2)

1
2
3
4
5
6
7
// ast.GenDecl 的结构
type GenDecl struct {
	Tok    token.Token   // 关键字:CONST、TYPE、VAR
	Lparen token.Pos      // 左圆括号位置(用于分组声明)
	Specs  []Spec        // 声明的规范列表
	Rparen token.Pos      // 右圆括号位置
}

批量声明的 AST 形态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

// 常量分组声明
const (
	Debug   = false
	Version = "1.0.0"
	Port    = 8080
)

// 类型分组声明
type (
	User struct {
		Name string
		Age  int
	}
	Status int
)

// 变量分组声明
var (
	globalVar int
	once      sync.Once
)`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	for _, decl := range f.Decls {
		if gd, ok := decl.(*ast.GenDecl); ok {
			fmt.Printf("声明类型: %s, Specs 数量: %d\n", gd.Tok, len(gd.Specs))

			for i, spec := range gd.Specs {
				switch s := spec.(type) {
				case *ast.ValueSpec:
					names := make([]string, len(s.Names))
					for j, n := range s.Names {
						names[j] = n.Name
					}
					values := make([]string, len(s.Values))
					for j, v := range s.Values {
						if v != nil {
							values[j] = v.(*ast.BasicLit).Value
						}
					}
					fmt.Printf("  [%d] ValueSpec: 名字=%v, 值=%v\n", i, names, values)

				case *ast.TypeSpec:
					fmt.Printf("  [%d] TypeSpec: 名字=%s\n", i, s.Name.Name)
				}
			}
			fmt.Println()
		}
	}
}
声明类型: CONST, Specs 数量: 3
  [0] ValueSpec: 名字=[Debug], 值=[false]
  [1] ValueSpec: 名字=[Version], 值=["1.0.0"]
  [2] ValueSpec: 名字=[Port], 值=[8080]

声明类型: TYPE, Specs 数量: 2
  [0] TypeSpec: 名字=User
  [1] TypeSpec: 名字=Status

声明类型: VAR, Specs 数量: 2
  [0] ValueSpec: 名字=[globalVar], 值=[]
  [1] ValueSpec: 名字=[once], 值=[]

GenDecl vs FuncDecl

graph TD
    A["Decl 接口"] --> B["*ast.GenDecl<br/>(const/type/var)"]
    A --> C["*ast.FuncDecl<br/>(func)"]
    
    B --> B1["Tok: CONST/TYPE/VAR"]
    B --> B2["Specs: []Spec"]
    B1 --> B3["单个声明用 ValueSpec/TypeSpec"]
    
    C --> C1["Name: *Ident"]
    C --> C2["Type: *FuncType"]
    C --> C3["Body: *BlockStmt"]

43.11 ast.IfStmt、ast.ForStmt、ast.RangeStmt:控制流语句

控制流语句家族

Go 的控制流语句在 AST 中有多种专门的节点类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// IfStmt: if 条件语句
type IfStmt struct {
	If   token.Pos   // if 关键字位置
	Cond Expr        // 条件表达式
	Body *BlockStmt  // then 分支
	Else Stmt        // else 分支(可以为 nil)
}

// ForStmt: for 循环语句
type ForStmt struct {
	For  token.Pos   // for 关键字位置
	Init Stmt        // 初始化语句(可以为 nil)
	Cond Expr        // 循环条件(可以为 nil,无条件=无限循环)
	Post Stmt        // 后置语句(每次迭代后执行)
	Body *BlockStmt  // 循环体
}

// RangeStmt: range 遍历语句
type RangeStmt struct {
	For    token.Pos   // for 关键字位置
	X      Expr        // 被遍历的表达式
	Tok    token.Token // = 或 :=(赋值/短变量声明)
	K, V   Expr        // 键和值(可以为 nil)
	Body   *BlockStmt  // 循环体
}

探索控制流语句的 AST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package main

func demo(x int, items []int) {
	// if 语句
	if x > 0 {
		fmt.Println("positive")
	} else if x < 0 {
		fmt.Println("negative")
	} else {
		fmt.Println("zero")
	}

	// for 循环的三种形式
	for {
		break
	}

	i := 0
	for i < 10 {
		i++
	}

	for j := 0; j < 5; j++ {
		fmt.Println(j)
	}

	// range 遍历
	for i, v := range items {
		fmt.Printf("%d: %d\n", i, v)
	}
}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	// 找到 demo 函数
	var funcDecl *ast.FuncDecl
	for _, decl := range f.Decls {
		if fd, ok := decl.(*ast.FuncDecl); ok && fd.Name.Name == "demo" {
			funcDecl = fd
			break
		}
	}

	if funcDecl != nil {
		ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
			switch stmt := n.(type) {
			case *ast.IfStmt:
				cond := exprString(stmt.Cond)
				hasElse := stmt.Else != nil
				fmt.Printf("IfStmt: 条件=%s, 有else=%v\n", cond, hasElse)

			case *ast.ForStmt:
				hasInit := stmt.Init != nil
				hasCond := stmt.Cond != nil
				hasPost := stmt.Post != nil
				condStr := "无(无限循环)"
				if stmt.Cond != nil {
					condStr = exprString(stmt.Cond)
				}
				fmt.Printf("ForStmt: 初始化=%v, 条件=%s, 后置=%v\n",
					hasInit, condStr, hasPost)

			case *ast.RangeStmt:
				fmt.Printf("RangeStmt: 被遍历=%s\n", exprString(stmt.X))
			}
			return true
		})
	}
}

// 辅助:简化表达式的字符串表示
func exprString(e ast.Expr) string {
	switch v := e.(type) {
	case *ast.Ident:
		return v.Name
	case *ast.BinaryExpr:
		return exprString(v.X) + " " + v.Op.String() + " " + exprString(v.Y)
	case *ast.ParenExpr:
		return "(" + exprString(v.X) + ")"
	default:
		return "..."
	}
}
IfStmt: 条件=x>0, 有else=true
IfStmt: 条件=x<0, 有else=false
ForStmt: 初始化=无(无限循环), 条件=无(无限循环), 后置=false
ForStmt: 初始化=false, 条件=i<10, 后置=false
ForStmt: 初始化=true, 条件=j<5, 后置=true
RangeStmt: 被遍历=items

流程控制语句对照

语句类型AST 节点关键字段
if 条件*ast.IfStmtCond, Body, Else
for 循环*ast.ForStmtInit, Cond, Post, Body
range 遍历*ast.RangeStmtX, K, V, Body
switch 分支*ast.SwitchStmtInit, Tag, Body
select 通信*ast.SelectStmtBody
goto 跳转*ast.BranchStmtTok (GOTO/BREAK/CONTINUE)

43.12 ast.Walk:遍历语法树,Visitor 模式

什么是 ast.Walk?

ast.Walk 是 Go 提供的一个语法树遍历工具。它使用 Visitor 模式:你定义一个"访问者"(实现 Visitor 接口),然后 ast.Walk 会自动遍历树中的每个节点,每遇到一个节点就调用你的访问者方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Visitor 接口
type Visitor interface {
	Visit(n Node) (w Visitor)
}

// Walk 函数
func Walk(v Visitor, n Node)

// Inspect 函数(更轻量,不递归)
func Inspect(n Node, f func(Node) bool)

自定义 Visitor 遍历 AST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

// FuncCounter 统计函数定义和调用的访问者
type FuncCounter struct {
	FuncDefs   int
	FuncCalls  int
	TotalNodes int
}

// Visit 实现 Visitor 接口
func (fc *FuncCounter) Visit(n ast.Node) ast.Visitor {
	fc.TotalNodes++
	switch n.(type) {
	case *ast.FuncDecl:
		fc.FuncDefs++
	case *ast.CallExpr:
		fc.FuncCalls++
	}
	return fc // 继续遍历子树
}

func main() {
	src := `package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	result := add(1, 2)
	fmt.Println(result)
	moreAdd := func(x, y int) int {
		return add(x, y)
	}
	fmt.Println(moreAdd(3, 4))
}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "main.go", src, 0)

	fc := &FuncCounter{}
	ast.Walk(fc, f)

	fmt.Printf("函数定义数: %d\n", fc.FuncDefs)
	fmt.Printf("函数调用数: %d\n", fc.FuncCalls)
	fmt.Printf("总节点数: %d\n", fc.TotalNodes)
}
函数定义数: 2
函数调用数: 4
总节点数: 87

经典应用:自动格式化字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
)

func main() {
	src := `package main
func hello(){fmt.Println("Hello")}`

	fset := token.NewFileSet()
	f, _ := parser.ParseFile(fset, "", src, 0)

	// 格式化并输出
	var buf bytes.Buffer
	printer.Fprint(&buf, fset, f)
	fmt.Println(buf.String())
}
package main

func hello() {
	fmt.Println("Hello")
}

Walk 执行流程图

sequenceDiagram
    participant W as ast.Walk
    participant V as Visitor (FuncCounter)
    participant N as Node
    
    W->>+N: 从根节点 File 开始
    N->>+V: Visit(File)
    V-->>-W: 返回 V(继续遍历)
    W->>+N: 遍历 Decls[]
    N->>+V: Visit(FuncDecl)
    V-->>-W: 返回 V
    W->>+N: 遍历 FuncDecl.Body.BlockStmt
    N->>+V: Visit(AssignStmt)
    V-->>-W: 返回 V
    Note over W,V: 递归遍历所有子节点...
    W->>+N: Visit(CallExpr)
    V-->>-W: 记录 FuncCalls++

术语解释

术语英文含义
Visitor 模式Visitor Pattern一种设计模式,将数据结构与操作分离
深度优先遍历DFS (Depth-First Search)优先访问子节点的遍历方式
WalkWalkast.Walk 函数,沿着树结构递归访问每个节点

43.13 go/parser 包:语法解析器

go/parser 包全景

go/parser 包是 Go 标准库中的语法解析器,它负责将源代码字符串解析成 *ast.File*ast.Package

graph TD
    A["go/parser 包"] --> B["ParseFile<br/>解析单个文件"]
    A --> C["ParseDir<br/>解析整个目录"]
    A --> D["ParseExpr<br/>解析单个表达式"]
    
    B --> E["返回 *ast.File"]
    C --> F["返回 *ast.Package"]
    D --> G["返回 ast.Expr"]
    
    style A fill:#FFD700
    style B fill:#90EE90
    style C fill:#90EE90
    style D fill:#90EE90

解析模式

parser.ParseFileparser.ParseDir 有一个 mode 参数,控制解析的行为:

1
2
3
4
5
6
7
8
const (
	ParseComments   Mode = 1 << iota // 解析注释
	StopAtError                      // 遇到错误停止
	Trace                            // 打印解析过程(调试用)
	AllErrors                        // 收集所有错误,不止第一个
	PublicErrors                     // 公开错误信息
	AllErrors | PublicErrors         // 最常用组合:收集所有错误
)

一个解析器诊断工具的雏形

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"fmt"
	"go/parser"
	"go/token"
)

func main() {
	// 故意写一个语法有问题的代码
	badSrc := `package main

func broken() {
	x := 1
	y := 2
	z := x + y
	return z  // 这里有问题:返回值类型不匹配
}`

	fset := token.NewFileSet()
	_, err := parser.ParseFile(fset, "broken.go", badSrc, parser.AllErrors)
	if err != nil {
		fmt.Println("解析错误:", err)
	}
	// Go 的类型检查不属于语法解析,这里只演示语法解析
	// 实际项目中类型检查需要 go/types 包
}
解析错误: broken.go:6:3: expected '}', found 'return'

43.14 parser.ParseFile:解析单个文件

ParseFile 详解

1
2
3
4
5
6
7
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)

// 参数说明:
// fset:      token 文件集(用于记录位置信息,必传)
// filename:  文件名(仅用于错误信息和 token 位置,可为 "")
// src:       源代码,可以是 string、[]byte 或 io.Reader
// mode:      解析模式标志位

完整使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"strings"
)

func main() {
	src := `package main

import (
	"fmt"
	"errors"
)

// Divide 安全的除法
func Divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := Divide(10, 0)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Result:", result)
}`

	fset := token.NewFileSet()

	// 解析文件(包含注释)
	f, err := parser.ParseFile(fset, "main.go", src, parser.ParseComments)
	if err != nil {
		panic(fmt.Sprintf("解析失败: %v", err))
	}

	// 打印文件概览
	fmt.Println("=== 文件概览 ===")
	fmt.Printf("包名: %s\n", f.Name.Name)
	fmt.Printf("导入包:\n")
	for _, imp := range f.Imports {
		path := strings.Trim(imp.Path.Value, `"`)
		alias := ""
		if imp.Name != nil {
			alias = " (as " + imp.Name.Name + ")"
		}
		fmt.Printf("  - %s%s\n", path, alias)
	}

	fmt.Printf("\n声明列表:\n")
	for _, decl := range f.Decls {
		switch d := decl.(type) {
		case *ast.FuncDecl:
			isMethod := d.Recv != nil
			recv := ""
			if isMethod {
				recv = " (method)"
			}
			fmt.Printf("  func %s%s\n", d.Name.Name, recv)
		case *ast.GenDecl:
			fmt.Printf("  %s declaration (包含 %d 个 spec)\n", d.Tok, len(d.Specs))
		}
	}

	// 打印注释
	fmt.Printf("\n注释:\n")
	if f.Doc != nil {
		for _, c := range f.Doc.List {
			fmt.Printf("  包文档: %s\n", c.Text)
		}
	}

	// 遍历并打印每个函数的文档
	for _, decl := range f.Decls {
		if fd, ok := decl.(*ast.FuncDecl); ok && fd.Name.Name != "main" {
			if fd.Doc != nil {
				fmt.Printf("  函数 %s 的文档: %s\n", fd.Name.Name, fd.Doc.Text())
			}
		}
	}
}
=== 文件概览 ===
包名: main
导入包:
  - fmt
  - errors

声明列表:
  func Divide (method)
  func main

注释:
  包文档: 
  函数 Divide 的文档: 安全的除法

43.15 parser.ParseDir:解析目录,返回 Package

ParseDir 详解

1
2
3
4
5
6
func ParseDir(fset *token.FileSet, dir string, filter func(os.FileInfo) bool, mode Mode) (pkg *ast.Package, err error)

// 参数说明:
// dir:     目录路径
// filter:  文件过滤器,返回 true 表示包含该文件
// mode:    解析模式

解析目录示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
	"fmt"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

func main() {
	// 查找当前目录
	cwd, err := os.Getwd()
	if err != nil {
		fmt.Println("获取当前目录失败:", err)
		return
	}

	fset := token.NewFileSet()

	// 解析整个包目录
	// 过滤掉 _test.go 文件
	pkg, err := parser.ParseDir(fset, cwd, func(info os.FileInfo) bool {
		return !strings.HasSuffix(info.Name(), "_test.go")
	}, parser.ParseComments)

	if err != nil {
		fmt.Println("解析目录失败:", err)
		return
	}

	// 遍历包
	for name, p := range pkg {
		fmt.Printf("包名: %s\n", name)
		fmt.Printf("文件数量: %d\n", len(p.Files))

		// 统计各种声明
		funcCount := 0
		typeCount := 0
		varCount := 0
		constCount := 0

		for fname, file := range p.Files {
			fmt.Printf("\n  文件: %s\n", filepath.Base(fname))
			for _, decl := range file.Decls {
				switch decl.(type) {
				case *ast.FuncDecl:
					funcCount++
				case *ast.GenDecl:
					// 进一步细分
					if gd, ok := decl.(*ast.GenDecl); ok {
						switch gd.Tok {
						case token.TYPE:
							typeCount++
						case token.VAR:
							varCount++
						case token.CONST:
							constCount++
						}
					}
				}
			}
		}

		fmt.Printf("\n=== 统计 ===\n")
		fmt.Printf("函数定义: %d\n", funcCount)
		fmt.Printf("类型定义: %d\n", typeCount)
		fmt.Printf("变量声明: %d\n", varCount)
		fmt.Printf("常量声明: %d\n", constCount)
	}
}

提示:运行此代码时,请确保当前目录下有真实的 .go 文件,否则输出会为空。

多文件包的 AST 结构

graph TD
    A["*ast.Package<br/>name=\"mypkg\""]
    
    A --> B["Files map<br/>文件集合"]
    A --> C["Scope<br/>包作用域"]
    
    B --> D["\"a.go\" → *ast.File"]
    B --> E["\"b.go\" → *ast.File"]
    B --> F["\"util.go\" → *ast.File"]
    
    D --> D1["Name: mypkg"]
    D --> D2["Decls: [import, func A]"]
    
    E --> E1["Name: mypkg"]
    E --> E2["Decls: [func B, type T]"]
    
    F --> F1["Name: mypkg"]
    F --> F2["Decls: [var x int, func helper]"]

43.16 parser.ParseExpr:解析单个表达式

ParseExpr 详解

1
2
3
4
5
6
func ParseExpr(x string) (Expr, error)
func ParseExprKind(fset *token.FileSet, filename string, src interface{}) (ast.Expr, error)

// 特点:
// - 只能解析表达式,不能解析语句或声明
// - 适合做表达式级别的工具,如公式解析、计算器、模板引擎

表达式解析的妙用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
)

func main() {
	// 解析各种表达式
	expressions := []string{
		`1 + 2`,
		`a > b && c < d`,
		`func(x int) int { return x * 2 }`,
		`[]int{1, 2, 3}`,
		`map[string]int{"a": 1}`,
		`Person{Name: "Tom", Age: 20}`,
	}

	for _, expr := range expressions {
		e, err := parser.ParseExpr(expr)
		if err != nil {
			fmt.Printf("表达式 %q 解析失败: %v\n", expr, err)
			continue
		}

		// 获取表达式类型
		exprType := getExprType(e)
		fmt.Printf("表达式: %-30s 类型: %s\n", expr, exprType)
	}
}

func getExprType(e ast.Expr) string {
	switch e.(type) {
	case *ast.Ident:
		return "*ast.Ident (标识符)"
	case *ast.BasicLit:
		return "*ast.BasicLit (基本字面量)"
	case *ast.BinaryExpr:
		return "*ast.BinaryExpr (二元表达式)"
	case *ast.UnaryExpr:
		return "*ast.UnaryExpr (一元表达式)"
	case *ast.ParenExpr:
		return "*ast.ParenExpr (括号表达式)"
	case *ast.FuncLit:
		return "*ast.FuncLit (函数字面量)"
	case *ast.CompositeLit:
		return "*ast.CompositeLit (复合字面量)"
	case *ast.CallExpr:
		return "*ast.CallExpr (函数调用)"
	case *ast.IndexExpr:
		return "*ast.IndexExpr (索引表达式)"
	case *ast.SelectorExpr:
		return "*ast.SelectorExpr (选择表达式)"
	default:
		return fmt.Sprintf("%T", e)
	}
}
表达式: 1 + 2                        类型: *ast.BinaryExpr (二元表达式)
表达式: a > b && c < d               类型: *ast.BinaryExpr (二元表达式)
表达式: func(x int) int { return x * 2 }  类型: *ast.FuncLit (函数字面量)
表达式: []int{1, 2, 3}               类型: *ast.CompositeLit (复合字面量)
表达式: map[string]int{"a": 1}        类型: *ast.CompositeLit (复合字面量)
表达式: Person{Name: "Tom", Age: 20}  类型: *ast.CompositeLit (复合字面量)

实用场景

场景说明
表达式计算器解析 1+2*3,构建 AST,求值得到 7
公式渲染解析数学公式,渲染成 LaTeX 或图片
模板引擎解析 {{.Name}},识别字段访问
条件解析解析用户输入的布尔条件,动态执行

本章小结

本章我们深入探索了 Go 标准库中用于语法分析的两大核心包:go/astgo/parser

核心概念回顾

概念说明
AST抽象语法树,以树形结构表示源代码的语法组成
Token词法分析的产物,源代码的最小语义单元
Node 接口所有 AST 节点的公共接口,包含 Pos()End()
Expr / Stmt / Decl三大节点类型接口,分别代表表达式、语句、声明

go/ast 核心节点速查

节点类型说明
*ast.File单个文件的完整 AST 表示
*ast.Package多文件包的 AST 集合
*ast.Ident标识符(变量名、函数名等)
*ast.BasicLit基本字面量(数字、字符串、字符)
*ast.FuncLit匿名函数(函数字面量)
*ast.CompositeLit复合字面量(T{...}
*ast.FuncDecl函数/方法声明
*ast.GenDecl通用声明(const / type / var)
*ast.IfStmtif 条件语句
*ast.ForStmtfor 循环语句
*ast.RangeStmtrange 遍历语句

go/parser 解析函数

函数返回值用途
ParseFile*ast.File解析单个 .go 文件
ParseDir*ast.Package解析整个包目录
ParseExprast.Expr解析单个表达式

关键技巧

  1. ast.Inspect:轻量级遍历,不需要实现完整的 Visitor 接口
  2. ast.Walk:配合自定义 Visitor,实现对语法树的递归遍历
  3. token.FileSet:每个解析操作都需要它来记录源码位置信息
  4. 模式标志parser.ParseComments 开启注释解析,parser.AllErrors 收集所有错误

应用场景

go/astgo/parser 是 Go 生态中众多工具的基础:

  • gofmt / go vet:代码格式化和静态分析
  • IDE 自动补全(gopls):理解代码结构,提供智能提示
  • 代码生成工具(如 stringermockgen):通过 AST 读取注解生成代码
  • 文档生成工具(godoc):解析注释生成文档
  • 自定义 linter:分析代码风格和安全问题

掌握了 AST,你就像拥有了一双"代码之眼",能够看到代码背后的结构之美,进而构建出强大的代码分析和处理工具。

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)