go/parser - Go 源码解析器
go/parser 包提供了 Go 源代码的解析功能,将源码字符串解析为抽象语法树(AST)。
概述
go/parser 包用于解析 Go 源代码并生成 AST,是 Go 代码分析工具的核心组件,与 go/ast 和 go/token 包配合使用。
包导入:
import (
"go/parser"
"go/ast"
"go/token"
"fmt"
)
基本使用:
// 1. 创建 FileSet
fset := token.NewFileSet()
// 2. 解析源码
src := `package main; func main() {}`
file, err := parser.ParseFile(fset, "main.go", src, 0)
if err != nil {
panic(err)
}
// 3. 遍历 AST
ast.Inspect(file, func(n ast.Node) bool {
fmt.Printf("%T\n", n)
return true
})
典型示例:
示例 1:解析字符串源码:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
src := `
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
`
// 创建 FileSet
fset := token.NewFileSet()
// 解析源码
file, err := parser.ParseFile(fset, "hello.go", src, parser.ParseComments)
if err != nil {
panic(err)
}
fmt.Printf("包名:%s\n", file.Name.Name)
fmt.Printf("文件:%s\n", fset.Position(file.Pos()).Filename)
fmt.Printf("声明数量:%d\n", len(file.Decls))
}
运行:
$ go run main.go
包名:main
文件:hello.go
声明数量:2
示例 2:解析文件并提取信息:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
// 解析当前目录的文件
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, ".", nil, parser.ParseComments)
if err != nil {
panic(err)
}
// 遍历所有包
for pkgName, pkg := range pkgs {
fmt.Printf("包:%s\n", pkgName)
fmt.Printf(" 文件数:%d\n", len(pkg.Files))
// 遍历包中的文件
for fileName, file := range pkg.Files {
fmt.Printf(" 文件:%s\n", fileName)
fmt.Printf(" 函数数:%d\n", len(file.Scope.Objects))
}
}
}
运行:
$ go run main.go
包:main
文件数:1
文件:main.go
函数数:2
一、Mode 类型
解析模式类型
Mode
定义:
type Mode uint
说明:
- 控制解析器的行为
- 使用位掩码组合多个选项
包级别常量
ParseComments
定义:
const ParseComments Mode = 1 << iota
说明:
- 解析注释(默认不解析)
- 注释存储在 AST 的 Doc 字段中
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
src := `
package main
// Hello 是问候函数
func Hello() {
// 打印问候语
println("Hello")
}
`
fset := token.NewFileSet()
// 不解析注释
file1, _ := parser.ParseFile(fset, "", src, 0)
fmt.Printf("无注释:%v\n", file1.Decls[0].(*ast.FuncDecl).Doc != nil)
// 解析注释
file2, _ := parser.ParseFile(fset, "", src, parser.ParseComments)
fmt.Printf("有注释:%v\n", file2.Decls[0].(*ast.FuncDecl).Doc != nil)
}
运行:
$ go run main.go
无注释:false
有注释:true
DeclarationErrors
定义:
const DeclarationErrors Mode = 1 << iota
说明:
- 报告声明错误
- 如重复声明、未使用等
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
src := `
package main
var x int
var x int // 重复声明
`
fset := token.NewFileSet()
// 不报告声明错误
_, err1 := parser.ParseFile(fset, "", src, 0)
fmt.Printf("无错误报告:%v\n", err1 == nil)
// 报告声明错误
_, err2 := parser.ParseFile(fset, "", src, parser.DeclarationErrors)
fmt.Printf("有错误报告:%v\n", err2 != nil)
}
运行:
$ go run main.go
无错误报告:true
有错误报告:true
AllErrors
定义:
const AllErrors Mode = 1 << iota
说明:
- 报告所有错误(不仅仅是第一个)
- 用于完整的错误检查
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
src := `
package main
func test( {
x := 1
y := 2
return x + y
}
`
fset := token.NewFileSet()
// 只报告第一个错误
_, err1 := parser.ParseFile(fset, "", src, 0)
fmt.Printf("单个错误:%v\n", err1)
// 报告所有错误
_, err2 := parser.ParseFile(fset, "", src, parser.AllErrors)
fmt.Printf("所有错误:%v\n", err2)
}
Trace
定义:
const Trace Mode = 1 << iota
说明:
- 启用解析跟踪
- 用于调试解析过程
- 输出到标准错误
SkipObjectResolution
定义:
const SkipObjectResolution Mode = 1 << iota
说明:
- 跳过对象解析
- 更快但不提供符号信息
二、包级别函数(按字母顺序)
解析表达式
ParseExpr
定义:
func ParseExpr(x string) (ast.Expr, error)
说明:
- 解析单个表达式
- 返回表达式 AST 节点
参数:
x:表达式字符串
返回值:
ast.Expr:表达式 AST 节点error:解析错误
示例:
package main
import (
"fmt"
"go/parser"
)
func main() {
// 解析简单表达式
expr1, _ := parser.ParseExpr("a + b")
fmt.Printf("类型:%T\n", expr1)
// 解析函数调用
expr2, _ := parser.ParseExpr("fmt.Println(x)")
fmt.Printf("类型:%T\n", expr2)
// 解析复杂表达式
expr3, _ := parser.ParseExpr("x.y[z]")
fmt.Printf("类型:%T\n", expr3)
}
运行:
$ go run main.go
类型:*ast.BinaryExpr
类型:*ast.CallExpr
类型:*ast.IndexExpr
解析文件
ParseFile
定义:
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (*ast.File, error)
说明:
- 解析单个 Go 源文件
- 最常用的解析函数
参数:
fset:token 文件集filename:文件名(用于错误信息)src:源码(可以是 string、[]byte、io.Reader 或 nil)mode:解析模式
返回值:
*ast.File:文件 ASTerror:解析错误
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
)
func main() {
fset := token.NewFileSet()
// 从字符串解析
src := `package main; func main() {}`
file1, _ := parser.ParseFile(fset, "test.go", src, 0)
fmt.Printf("字符串:%s\n", file1.Name.Name)
// 从字节切片解析
srcBytes := []byte(`package main; var x int`)
file2, _ := parser.ParseFile(fset, "test.go", srcBytes, 0)
fmt.Printf("字节:%s\n", file2.Name.Name)
// 从文件解析
file3, _ := parser.ParseFile(fset, "main.go", nil, 0)
if file3 != nil {
fmt.Printf("文件:%s\n", file3.Name.Name)
}
}
解析目录
ParseDir
定义:
func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (map[string]*ast.Package, error)
说明:
- 解析目录中的所有 Go 文件
- 返回包名到包 AST 的映射
参数:
fset:token 文件集path:目录路径filter:文件过滤器(可为 nil)mode:解析模式
返回值:
map[string]*ast.Package:包名到包 AST 的映射error:解析错误
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
// 解析当前目录
pkgs, err := parser.ParseDir(fset, ".", nil, parser.ParseComments)
if err != nil {
panic(err)
}
// 遍历所有包
for name, pkg := range pkgs {
fmt.Printf("包:%s\n", name)
fmt.Printf(" 文件数:%d\n", len(pkg.Files))
// 遍历文件
for fname := range pkg.Files {
fmt.Printf(" - %s\n", fname)
}
}
}
运行:
$ go run main.go
包:main
文件数:1
- main.go
带过滤器的目录解析
ParseDirWithFilter
定义:
func ParseDirWithFilter(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (map[string]*ast.Package, error)
说明:
- 与 ParseDir 相同,但支持文件过滤
示例:
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
"strings"
)
func main() {
fset := token.NewFileSet()
// 只解析 _test.go 文件
filter := func(info os.FileInfo) bool {
return strings.HasSuffix(info.Name(), "_test.go")
}
pkgs, _ := parser.ParseDir(fset, ".", filter, 0)
for name, pkg := range pkgs {
fmt.Printf("测试包:%s\n", name)
fmt.Printf(" 测试文件数:%d\n", len(pkg.Files))
}
}
三、快速参考
Mode 常量
| 常量 | 说明 | 使用场景 |
|---|---|---|
| ParseComments | 解析注释 | 需要提取文档 |
| DeclarationErrors | 报告声明错误 | 完整错误检查 |
| AllErrors | 报告所有错误 | 完整错误列表 |
| Trace | 启用解析跟踪 | 调试解析过程 |
| SkipObjectResolution | 跳过对象解析 | 快速解析 |
包级别函数
| 函数 | 说明 | 输入 | 输出 |
|---|---|---|---|
| ParseExpr(x) | 解析表达式 | string | (ast.Expr, error) |
| ParseFile(fset, filename, src, mode) | 解析文件 | *FileSet, string, interface{}, Mode | (*ast.File, error) |
| ParseDir(fset, path, filter, mode) | 解析目录 | *FileSet, string, Filter, Mode | (map[string]*Package, error) |
src 参数类型
| 类型 | 说明 | 示例 |
|---|---|---|
string | 源码字符串 | parser.ParseFile(fset, "", "package main", 0) |
[]byte | 源码字节切片 | parser.ParseFile(fset, "", []byte(code), 0) |
io.Reader | 源码读取器 | parser.ParseFile(fset, "", reader, 0) |
nil | 从文件读取 | parser.ParseFile(fset, "main.go", nil, 0) |
使用场景
| 场景 | 推荐函数 | 模式 |
|---|---|---|
| 解析单个表达式 | ParseExpr | 0 |
| 解析源码字符串 | ParseFile | ParseComments |
| 解析文件 | ParseFile | 0 |
| 解析整个目录 | ParseDir | ParseComments |
| 只解析测试文件 | ParseDir | 0 + 过滤器 |
| 快速解析 | ParseFile | SkipObjectResolution |
| 完整错误检查 | ParseFile | AllErrors |
常见错误
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
| expected ‘;’ | 缺少分号 | Go 会自动添加,检查语法 |
| expected ‘}’ | 缺少右括号 | 检查括号匹配 |
| expected operand | 缺少操作数 | 检查表达式完整性 |
| expected type | 缺少类型 | 检查类型声明 |
四、最佳实践
1. 解析并遍历 AST
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func parseAndWalk(filename string, src string) error {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
if err != nil {
return err
}
ast.Inspect(file, func(n ast.Node) bool {
if fn, ok := n.(*ast.FuncDecl); ok {
fmt.Printf("函数:%s\n", fn.Name.Name)
}
return true
})
return nil
}
2. 提取包文档
package main
import (
"fmt"
"go/parser"
"go/token"
)
func extractPackageDoc(filename string, src string) {
fset := token.NewFileSet()
file, _ := parser.ParseFile(fset, filename, src, parser.ParseComments)
if file.Doc != nil {
fmt.Printf("包文档:%s\n", file.Doc.Text())
}
}
3. 错误处理和报告
package main
import (
"fmt"
"go/parser"
"go/token"
)
func safeParse(src string) {
fset := token.NewFileSet()
_, err := parser.ParseFile(fset, "test.go", src, parser.AllErrors)
if err != nil {
// 打印详细错误信息
fmt.Printf("解析错误:%v\n", err)
// 获取错误位置
if pos, ok := err.(interface{ Pos() token.Pos }); ok {
fmt.Printf("位置:%s\n", fset.Position(pos.Pos()))
}
}
}
4. 解析多个文件
package main
import (
"fmt"
"go/parser"
"go/token"
)
func parseMultipleFiles(files map[string]string) {
fset := token.NewFileSet()
for filename, src := range files {
file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
if err != nil {
fmt.Printf("解析 %s 失败:%v\n", filename, err)
continue
}
fmt.Printf("解析 %s 成功:%d 个声明\n",
filename, len(file.Decls))
}
}
5. 自定义文件过滤器
package main
import (
"fmt"
"go/parser"
"go/token"
"os"
"strings"
)
func parseWithFilter(dir string) {
fset := token.NewFileSet()
// 只解析非测试文件
filter := func(info os.FileInfo) bool {
return !strings.HasSuffix(info.Name(), "_test.go")
}
pkgs, err := parser.ParseDir(fset, dir, filter, parser.ParseComments)
if err != nil {
panic(err)
}
for name, pkg := range pkgs {
fmt.Printf("包 %s: %d 个文件\n", name, len(pkg.Files))
}
}
五、注意事项
1. FileSet 必须共享
// 错误:创建多个 FileSet
fset1 := token.NewFileSet()
file1, _ := parser.ParseFile(fset1, "a.go", src1, 0)
fset2 := token.NewFileSet()
file2, _ := parser.ParseFile(fset2, "b.go", src2, 0)
// 位置信息无法比较
// 正确:共享 FileSet
fset := token.NewFileSet()
file1, _ := parser.ParseFile(fset, "a.go", src1, 0)
file2, _ := parser.ParseFile(fset, "b.go", src2, 0)
2. 解析模式的选择
// 只需要 AST 结构
file, _ := parser.ParseFile(fset, "", src, 0)
// 需要文档注释
file, _ = parser.ParseFile(fset, "", src, parser.ParseComments)
// 需要完整错误检查
file, _ = parser.ParseFile(fset, "", src, parser.AllErrors)
3. 内存考虑
- 解析大型项目会消耗大量内存
- 使用 SkipObjectResolution 可以减少内存使用
- 考虑分批次解析文件
最后更新:2026-04-04
Go 版本:Go 1.23+