前言
在上一篇文章中,分享了 Go 编译器是如何将源文件解析成 Token 的。本文主要是分享,语法分析阶段是如何根据不同的 Token 来进行语法解析的。本文你可以了解到以下内容:
Go 语法分析整体概览
Go 语法解析详细过程
示例展示语法解析完整过程
💡 Tips:文中会涉及到文法、产生式相关内容,如果你不太了解,请先看一下前边的词法分析&语法分析基础这篇文章
文章比较长,但代码比较多,主要是方便理解。相信看完一定有所收获
Go 语法分析整体概览
为了方便后边的理解,我这里提供一个源文件,后边的内容,你都可以按照这个源文件中的内容,带入去理解:
package main
import (
"fmt"
"go/token"
)
type aType string
const A = 666
var B = 888
func main() {
fmt.Println("Test Parser")
token.NewFileSet()
}
复制代码
入口
在上一篇文章介绍词法分析的时候,详细的说明了 Go 的编译入口,在编译入口处初始化了语法分析器,并且在初始化语法分析器的过程中初始化了词法分析器,所以词法分析器是内嵌在了语法分词器的内部的,我们可以在:src/cmd/compile/internal/syntax/parser.go 中看到语法分析器的结构如下:
type parser struct {
file *PosBase //记录打开的文件的信息的(比如文件名、行、列信息)
errh ErrorHandler //报错回调
mode Mode //语法分析模式
pragh PragmaHandler
scanner //词法分析器
base *PosBase // current position base
first error // first error encountered
errcnt int // number of errors encountered
pragma Pragma // pragmas
fnest int // function nesting level (for error handling) 函数的嵌套层级
xnest int // expression nesting level (for complit ambiguity resolution) 表达式嵌套级别
indent []byte // tracing support(跟踪支持)
}
复制代码
因为在上一篇文章中已经详细的分享了入口文件的位置及它都做了什么(src/cmd/compile/internal/syntax/syntax.go → Parse(...)),在语法分析器和词法分析器初始化完成后,我们会看到它调用了语法分析器的fileOrNil()
方法,它就是语法分析的核心方法,下边就具体介绍一下这个核心方法具体做了哪些事情
Go 语法解析结构
这部分会涉及到每种声明类型的解析、及每种声明类型的结构体,还包括语法解析器生成的语法树的结点之间的关系。有些地方确实比较难理解,你可先大致看一遍文字内容,然后再结合我下边的那张语法解析的图,理解起来可能就轻松一些
Go 语法分析中的文法规则
我先对这部分做整体的介绍,然后再去了解里边的细节
进入到fileOrNil()
方法,在注释中会看到这么一行注释
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
复制代码
它就是 Go 解析源文件的文法规则,这个在系列文章的第二篇词法分析和语法分析基础部分有提到。Go 的编译器在初始化完语法分析器和词法分析器之后,就会调用该方法,该方法会通过词法分词器提供的 next()方法,来不断的获取 Token,语法分析就按照上边这个文法规则进行语法分析
可能你不知道 PackageClause、ImportDecl、TopLevelDecl 的产生式,你可以直接在这个文件中找到这三个产生式(关于产生式,在词法分析和语法分析基础部分有介绍)
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
PackageClause = "package" PackageName .
PackageName = identifier .
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec = [ "." | PackageName ] ImportPath .
ImportPath = string_lit .
TopLevelDecl = Declaration | FunctionDecl | MethodDecl .
Declaration = ConstDecl | TypeDecl | VarDecl .
ConstDecl = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .
AliasDecl = identifier "=" Type .
TypeDef = identifier Type .
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
复制代码
fileOrNil()
说白了就是按照 SourceFile 这个文法来对源文件进行解析,最终返回的是一个语法树(File 结构,下边会介绍),我们知道,编译器会将每一个文件都解析成一颗语法树
下边就简单的介绍一下 Go 语法分析中的几个文法的含义(如果你看了前边的词法分析和语法分析基础这篇文章,应该很容易就能看懂 Go 这些文法的含义)
SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
我们可以看到SourceFile是由PackageClause、ImportDecl、TopLevelDecl这三个非终结符号构成的。它的意思就是:
每一个源文件主要是由package声明、导入声明、和顶层声明构成的(其中ImportDecl和TopLevelDecl是可选的,可有可无,这就是中括号的含义)。也就是说,每一个源文件,都应该是符合这个文法规则的
首先是包声明:PackageClause
PackageClause = "package" PackageName .
PackageName = identifier .
PackageClause是由一个终结符package和一个非终结符PackageName构成的,而PackageName由一个标识符构成的
所以,在扫描源文件的时候,应该会最先获取到的是package的Token,然后是一个标识符的Token。解析完package声明之后,后边就应该是导入声明
然后是导入声明:ImportDecl
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec = [ "." | PackageName ] ImportPath .
ImportPath = string_lit .
复制代码
了解了上边这些文法的含义之后,下边就看 fileNil()这个方法是如何按照上边的文法进行语法解析的。但是在这之前,需要先知道 fileOrNil()这个方法生成的语法树的各个节点结构是什么样的
语法树各个节点的结构
我们知道,fileOrNil()方法会按照 SourceFile 的文法规则,生成一棵语法树,而每棵语法树的结构是这样的一个结构体:(src/cmd/compile/internal/syntax/nodes.go → File)
type File struct {
Pragma Pragma
PkgName *Name
DeclList []Decl
Lines uint
node
}
复制代码
它主要包含的是源文件的包名(PkgName)、和源文件中所有的声明(DeclList)。需要注意的是,它会将 import 也当做声明解析到 DeclList 中
fileOrNil()会将源文件中的所有声明(比如 var、type、const)按照每种声明的结构(每种声明都定义的有相应的结构体,用来保存声明信息,而这些结构体都是语法树的子节点)解析到 DeclList 中。在上边的文法中,我们能看到常量、类型、变量声明文法,其实还有函数和方法的文法,可以在 src/cmd/compile/internal/syntax/parser.go 中找到
FunctionDecl = "func" FunctionName ( Function | Signature ) .
FunctionName = identifier .
Function = Signature FunctionBody .
MethodDecl = "func" Receiver MethodName ( Function | Signature ) .
Receiver = Parameters .
复制代码
这棵语法树的根节点结构是 File 结构体,它的子节点结构就是:
ImportDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
LocalPkgName *Name // including "."; nil means no rename present
Path *BasicLit
decl
}
ConstDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
NameList []*Name
Type Expr // nil means no type
Values Expr // nil means no values
decl
}
TypeDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
Name *Name
Alias bool
Type Expr
decl
}
VarDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
NameList []*Name
Type Expr // nil means no type
Values Expr // nil means no values
decl
}
FuncDecl struct {
Pragma Pragma
Recv *Field // nil means regular function
Name *Name
Type *FuncType
Body *BlockStmt // nil means no body (forward declaration)
decl
}
复制代码
这些节点的结构定义在:src/cmd/compile/internal/syntax/nodes.go 中。也就是说在解析的过程中,如果解析到满足 ImportDecl 文法规则的,它就会创建相应结构的节点来保存相关信息;遇到满足 var 类型声明文法的,就创建 var 类型声明相关的结构体(VarDecl)来保存声明信息
如果解析到函数就稍微复杂一点,从函数节点的结构可以看到它包含接收者、函数名称、类型、函数体这几部分,最复杂的地方在函数体,它是一个 BlockStmt 的结构:
BlockStmt struct {
List []Stmt //Stmt是一个接口
Rbrace Pos
stmt
}
复制代码
BlockStmt 是由一系列的声明和表达式构成的,你在 src/cmd/compile/internal/syntax/nodes.go 中可以看到很多表达式和声明的结构体(这些结构体,也都是函数声明下边的节点结构)
// ----------------------------------------------------------------------------
// Statements
......
SendStmt struct {
Chan, Value Expr // Chan <- Value
simpleStmt
}
DeclStmt struct {
DeclList []Decl
stmt
}
AssignStmt struct {
Op Operator // 0 means no operation
Lhs, Rhs Expr // Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
simpleStmt
}
......
ReturnStmt struct {
Results Expr // nil means no explicit return values
stmt
}
IfStmt struct {
Init SimpleStmt
Cond Expr
Then *BlockStmt
Else Stmt // either nil, *IfStmt, or *BlockStmt
stmt
}
ForStmt struct {
Init SimpleStmt // incl. *RangeClause
Cond Expr
Post SimpleStmt
Body *BlockStmt
stmt
}
......
// ----------------------------------------------------------------------------
// Expressions
......
// [Len]Elem
ArrayType struct {
// TODO(gri) consider using Name{"..."} instead of nil (permits attaching of comments)
Len Expr // nil means Len is ...
Elem Expr
expr
}
// []Elem
SliceType struct {
Elem Expr
expr
}
// ...Elem
DotsType struct {
Elem Expr
expr
}
// struct { FieldList[0] TagList[0]; FieldList[1] TagList[1]; ... }
StructType struct {
FieldList []*Field
TagList []*BasicLit // i >= len(TagList) || TagList[i] == nil means no tag for field i
expr
}
......
// X[Index]
IndexExpr struct {
X Expr
Index Expr
expr
}
// X[Index[0] : Index[1] : Index[2]]
SliceExpr struct {
X Expr
Index [3]Expr
// Full indicates whether this is a simple or full slice expression.
// In a valid AST, this is equivalent to Index[2] != nil.
// TODO(mdempsky): This is only needed to report the "3-index
// slice of string" error when Index[2] is missing.
Full bool
expr
}
// X.(Type)
AssertExpr struct {
X Expr
Type Expr
expr
}
......
复制代码
是不是看着很眼熟,有 if、for、return 这些。BlockStmt 中的 Stmt 是一个接口类型,也就意味着上边的各种表达式类型或声明类型结构体都可以实现 Stmt 接口
知道了语法树中各个节点之间的关系,以及这些节点都可能会有哪些结构,下边就看 fileOrNil 是如何一步一步的解析出这棵语法树的各个节点的
fileOrNil() 的源码实现
这部分我不会深入到每一个函数里边去看它是怎么解析的,只是从大致轮廓上介绍它是怎么一步一步解析源文件中的 token 的,因为这部分主要是先从整体上认识。具体它是怎么解析 import、var、const、func 的,我会在下一部分详细的介绍
可以看到 fileOrNil()的代码实现主要包含以下几个部分:
// SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
func (p *parser) fileOrNil() *File {
......
//1.创建File结构体
f := new(File)
f.pos = p.pos()
// 2. 首先解析文件开头的package定义
// PackageClause
if !p.got(_Package) { //检查第一行是不是先定义了package
p.syntaxError("package statement must be first")
return nil
}
// 3. 当解析完package之后,解析import声明(每一个import在解析器看来都是一个声明语句)
// { ImportDecl ";" }
for p.got(_Import) {
f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
p.want(_Semi)
}
// 4. 根据获取的token去switch选择相应的分支,去解析对应类型的语句
// { TopLevelDecl ";" }
for p.tok != _EOF {
switch p.tok {
case _Const:
p.next() // 获取到下一个token
f.DeclList = p.appendGroup(f.DeclList, p.constDecl)
......
}
// p.tok == _EOF
p.clearPragma()
f.Lines = p.line
return f
}
复制代码
在 fileOrNil()中有两个比较重要的方法,是理解 fileOrNil()在做的事情的关键:
首先是 got 函数,它的参数是一个 token,用来判断从词法分析器获取到的 token 是不是参数中传入的这个 token
然后是 appendGroup 函数,它有两个参数,第一个是 DeclList(前边介绍 File 的结构体的成员时介绍过,它是用来存放源文件中所有的声明的,是一个切片类型);第二个参数是一个函数,这个函数是每种类型声明语句的分析函数(比如,我当前解析到 import 声明语句,那我就将解析 import 的方法作为第二个参数,传递给 appendGroup)
在解析 import 声明语句的时候,是下边这么一段代码:
for p.got(_Import) {
f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
p.want(_Semi)
}
复制代码
appendGroup 的作用其实就是找出批量的定义,就比如下边这些情况
//import的批量声明情况
import (
"fmt"
"io"
"strconv"
"strings"
)
//var的批量声明情况
var (
x int
y int
)
复制代码
对于存在批量声明情况的声明语句结构体中,它会有一个Group
字段,用来标明这些变量是属于同一个组,比如 import 的声明结构体和 var 的声明结构体
ImportDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
LocalPkgName *Name // including "."; nil means no rename present
Path *BasicLit
decl
}
VarDecl struct {
Group *Group // nil means not part of a group
Pragma Pragma
NameList []*Name
Type Expr // nil means no type
Values Expr // nil means no values
decl
}
复制代码
在 appendGroup 的方法里边,会调用相应声明类型的解析方法,跟 fileOrNil()一样,按照相应类型声明的文法进行解析。比如对 import 声明进行解析的方法importDecl()
for p.got(_Import) {
f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
p.want(_Semi)
}
......
// ImportSpec = [ "." | PackageName ] ImportPath .
// ImportPath = string_lit .
func (p *parser) importDecl(group *Group) Decl {
if trace {
defer p.trace("importDecl")()
}
d := new(ImportDecl)
d.pos = p.pos()
d.Group = group
d.Pragma = p.takePragma()
switch p.tok {
case _Name:
d.LocalPkgName = p.name()
case _Dot:
d.LocalPkgName = p.newName(".")
p.next()
}
d.Path = p.oliteral()
if d.Path == nil {
p.syntaxError("missing import path")
p.advance(_Semi, _Rparen)
return nil
}
return d
}
复制代码
我们可以看见,它也是先创建相应声明(这里是 import 声明)的结构体,然后记录声明的信息,并按照该种声明的文法对后边的内容进行解析
其它的还有像 const、type、var、func 等声明语句,通过 switch 去匹配并解析。他们也有相应的解析方法(其实就是按照各自的文法规则实现的代码),我这里不都列出来,你可以自己在 src/cmd/compile/internal/syntax/parser.go 中查看
前边我们说到,语法分析器最终会使用不通的结构体来构建语法树的各个节点,其中根节点就是:src/cmd/compile/internal/syntax/nodes.go → File。在前边已经介绍了它的结构,主要包含包名和所有的声明类型,这些不同类型的声明,就是语法树的子节点
可能通过上边的文字描述,稍微还是有点难理解,下边就通过图的方式来展示整个语法解析的过程是什么样的
图解语法分析过程
以文章开头提供的源代码为例来展示语法解析的过程。在Go词法分析这篇文章中有提到,Go 的编译入口是从 src/cmd/compile/internal/gc/noder.go → parseFiles 开始的
到这里相信你对 Go 的语法分析部分已经有个整体上的认识了。但是上边并没有画出各种声明语句是如何往下解析的,这就需要深入的去看每种声明语句的解析方法是如何实现
Go 语法解析详细过程
关于 Go 语法分析中各种声明及表达式的解析,你都可以在 src/cmd/compile/internal/syntax/parser.go 中找到对应的方法
变量声明 &导入声明解析
变量声明相关解析方法的调用,在 src/cmd/compile/internal/syntax/parser.go→fileOrNil()
方法中都可以找到
导入声明解析
在 src/cmd/compile/internal/syntax/parser.go→fileOrNil()
中可以看到下边这段代码
for p.got(_Import) {
f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
p.want(_Semi)
}
复制代码
在这个 appendGroup 中,最终会调用 importDecl()这个方法(从上边这段代码可以发现,当匹配到 import 的 token 之后才会执行导入声明的解析方法)
首先需要知道的是,import 有以下几种使用方式:
import "a" //默认的导入方式,a是包名
import aAlias "x/a" // 给x/a这个包起个别名叫aAlias
import . "c" // 将依赖包的公开符号直接导入到当前文件的名字空间
import _ "d" // 只是导入依赖包触发其包的初始化动作,但是不导入任何符号到当前文件名字空间
复制代码
因为篇幅的原因,我就不把 importDecl()方法的源代码粘出来了。我这里梳理出来它主要干了哪些事情:
创建一个 ImportDeal 的结构体(最终会被 append 到 File.DeclList 中)
初始化结构体中的一些数据,比如解析到的 token 的位置信息、组(group)等
然后去匹配下一个 token,看它是_Name
的 token 类型(标识符),还是_Dot(.)
如果获取到的 token 是_Name,
则获取到包名,如果获取到的是_Dot(.)
,则新建一个名字.
然后就是匹配包路径,主要是由oliteral()
方法实现
返回 ImportDeal 结构
这里值得说的是oliteral()
方法,它会获取到下一个 token,看它是不是一个基础面值类型的 token,也就是_Literal
💡 Tips:基础面值只有整数、浮点数、复数、符文和字符串几种类型
如果是基础面值类型,它就会创建一个基础面值类型的结构体 nodes.BasicLit,并初始化它的一些信息
BasicLit struct {
Value string //值
Kind LitKind //那种类型的基础面值,范围(IntLit、FloatLit、ImagLit、RuneLit、StringLit)
Bad bool // true means the literal Value has syntax errors
expr
}
复制代码
这就不难看出,当解析到基础面值类型的时候,它就已经是不可在分解的了,就是文法中说的终结符。在语法树上,这些终结符都是叶子节点
在 go 的标准库中有提供相关的方法来测试一下语法解析,我这里通过 go/parser 提供的接口来测一下导入的时候,语法分析的结果:
💡 Tips:跟词法分析一样(你可以看一下Go词法分析器这篇文章的标准库测试部分),标准库中语法分析的实现和 go 编译器中的实现不一样,主要是结构体的设计(比如跟结点的结构变了,你可以自己去看一下,比较简单,你明白了编译器中语法分析的实现,标准库里的也能看懂),实现思想是一样的
package main
import (
"fmt"
"go/parser"
"go/token"
)
const src = `
package test
import "a"
import aAlias "x/a"
import . "c"
import _ "d"
`
func main() {
fileSet := token.NewFileSet()
f, err := parser.ParseFile(fileSet, "", src, parser.ImportsOnly) //parser.ImportsOnly模式,表示只解析包声明和导入
if err != nil {
panic(err.Error())
}
for _, s := range f.Imports{
fmt.Printf("import:name = %v, path = %#v\n", s.Name, s.Path)
}
}
复制代码
打印结果:
import:name = <nil>, path = &ast.BasicLit{ValuePos:22, Kind:9, Value:"\"a\""}
import:name = aAlias, path = &ast.BasicLit{ValuePos:40, Kind:9, Value:"\"x/a\""}
import:name = ., path = &ast.BasicLit{ValuePos:55, Kind:9, Value:"\"c\""}
import:name = _, path = &ast.BasicLit{ValuePos:68, Kind:9, Value:"\"d\""}
复制代码
后边的各种类型声明解析或表达式的解析,你都可以通过标准库里边提供的方法来进行测试,下边的我就不一一的展示测试了(测试方法一样,只是你需要打印的字段变一下就行了)
type 类型声明解析
当语法分析器获取到_Type
这个 token 的时候,它就会调用 type 的解析方法去解析,同样在 fileOrNil()中,你可以看到如下代码:
......
case _Type:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)
......
复制代码
它会在 appendGroup 中调用 typeDecl()方法,该方法就是按照 type 类型声明的文法去进行语法解析,这个在前边已经介绍了。我们知道 type 有以下用法:
type a string
type b = string
复制代码
下边看这个方法中具体做了哪些事情:
创建一个TypeDecl
的结构体(最终会被 append 到 File.DeclList 中)
初始化结构体中的一些数据,比如解析到的 token 的位置信息、组(group)等
下一个 token 是否是_Assign
,如果是,则获取下一个 token
验证下一个 token 的类型,比如是 chan、struct、map 还是 func 等(在typeOrNil
()方法中实现的,其实就是一堆 switch case)
返回TypeDecl
结构
在获取最右边那个 token 的类型的时候,需要根据它的类型,继续往下解析。假设是一个 chan 类型,那就会创建一个 chan 类型的结构体,初始化这个 chan 类型的信息(在解析过程中,创建的每一种类型的结构体,都是一个结点)
你同样可以用 go/parser→ParseFile 来测试 type 的语法分析
const 类型声明解析
当语法分析器获取到_Const
这个 token 的时候,它就会调用 const 的解析方法去解析,同样在 fileOrNil()中,你可以看到如下代码:
......
case _Const:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.constDecl)
......
复制代码
const 有以下用法:
const A = 666
const B float64 = 6.66
const (
_ token = iota
_EOF
_Name
_Literal
)
复制代码
然后就可以去看constDecl
方法中的具体实现(其实按照 const 声明的文法进行解析)。这里就不再重复了,跟上边 type 的解析差不多,都是先创建相应结构体,然后记录该类型的一些信息。它不一样的地方就是,它有个名字列表,因为 const 可以同时声明多个。解析方法是 parser.go→nameList
()
var 类型声明解析
当语法分析器获取到_Var
这个 token 的时候,它就会调用 var 的解析方法去解析,同样在 fileOrNil()中,你可以看到如下代码:
......
case _Var:
p.next()
f.DeclList = p.appendGroup(f.DeclList, p.varDecl)
......
复制代码
跟上边的两种声明一样,会调用相应的解析方法。var 不同的是,它的声明里边,可能涉及表达式,所以在 var 的解析方法中涉及到表达式的解析,我会在后边部分详细分析表达式的解析
函数声明解析实现
💡 说明:后边会加个图来展示函数解析的过程
最后就是函数声明的解析。前边已经提到,函数声明节点的结构如下:
FuncDecl struct {
Pragma Pragma
Recv *Field // 接收者
Name *Name //函数名
Type *FuncType //函数类型
Body *BlockStmt // 函数体
decl
}
复制代码
在 fileOrNil()中,你可以看到如下代码:
case _Func:
p.next()
if d := p.funcDeclOrNil(); d != nil {
f.DeclList = append(f.DeclList, d)
}
复制代码
解析函数的核心方法就是 funcDeclOrNil,因为函数的解析稍微复杂点,我这里把它的实现粘出来,通过注释来说明每行代码在做什么
// 函数的文法
// FunctionDecl = "func" FunctionName ( Function | Signature ) .
// FunctionName = identifier .
// Function = Signature FunctionBody .
// 方法的文法
// MethodDecl = "func" Receiver MethodName ( Function | Signature ) .
// Receiver = Parameters . //方法的接收者
func (p *parser) funcDeclOrNil() *FuncDecl {
if trace {
defer p.trace("funcDecl")()
}
f := new(FuncDecl) //创建函数声明类型的结构体(节点)
f.pos = p.pos()
f.Pragma = p.takePragma()
if p.tok == _Lparen { //如果匹配到了左小括号(说明是方法)
rcvr := p.paramList() //获取接收者列表
switch len(rcvr) {
case 0:
p.error("method has no receiver")
default:
p.error("method has multiple receivers")
fallthrough
case 1:
f.Recv = rcvr[0]
}
}
if p.tok != _Name { //判断下一个token是否是标识符(即函数名)
p.syntaxError("expecting name or (")
p.advance(_Lbrace, _Semi)
return nil
}
f.Name = p.name()
f.Type = p.funcType() //获取类型(下边继续了解其内部实现)
if p.tok == _Lbrace { // 如果匹配到左中括号,则开始解析函数体
f.Body = p.funcBody() //解析函数体(下边继续了解其内部实现)
}
return f
}
复制代码
函数解析部分比较重要的两个实现:funcType()
、funcBody()
。具体看他们内部做了什么?
/*
FuncType struct {
ParamList []*Field
ResultList []*Field
expr
}
*/
func (p *parser) funcType() *FuncType {
if trace {
defer p.trace("funcType")()
}
typ := new(FuncType) //创建函数类型结构体(主要成员是参数列表和返回值列表)
typ.pos = p.pos()
typ.ParamList = p.paramList() //获取参数列表(它返回的是一个Field结构体,它的成员是参数名和类型)
typ.ResultList = p.funcResult() //获取返回值列表(它返回的也是一个Field结构体)
return typ
}
复制代码
func (p *parser) funcBody() *BlockStmt {
p.fnest++ // 记录函数的调用层级
errcnt := p.errcnt // 记录当前的错误数
body := p.blockStmt("") // 解析函数体中的语句(具体实现继续往下看)
p.fnest--
// Don't check branches if there were syntax errors in the function
// as it may lead to spurious errors (e.g., see test/switch2.go) or
// possibly crashes due to incomplete syntax trees.
if p.mode&CheckBranches != 0 && errcnt == p.errcnt {
checkBranches(body, p.errh)
}
return body
}
func (p *parser) blockStmt(context string) *BlockStmt {
if trace {
defer p.trace("blockStmt")()
}
s := new(BlockStmt) //创建函数体的结构
s.pos = p.pos()
// people coming from C may forget that braces are mandatory in Go
if !p.got(_Lbrace) {
p.syntaxError("expecting { after " + context)
p.advance(_Name, _Rbrace)
s.Rbrace = p.pos() // in case we found "}"
if p.got(_Rbrace) {
return s
}
}
s.List = p.stmtList() //开始解析函数体中的声明及表达式(这里边的实现就是根据获取的token来判断是哪种声明或语句,也是通过switch case来实现,根据匹配的类型进行相应文法的解析)
s.Rbrace = p.pos()
p.want(_Rbrace)
return s
}
复制代码
上边函数解析过程用图来展示一下,方便理解:
关于在函数体中的一些像赋值、for、go、defer、select 等等是如何解析,可以自行去看
总结
本文主要是从整体上分享了语法解析的过程,并且简单的展示了 type、const、func 声明解析的内部实现。其实语法解析里边还有表达式解析、包括其他的一些语法的解析,内容比较多,没法一一介绍,感兴趣的小伙伴可以自行研究
感谢阅读,下一篇主题为:抽象语法树构建
评论