前言

在上一篇文章中,分享了Go编译器是如何将源文件解析成Token的。本文主要是分享,语法分析阶段是如何根据不同的Token来进行语法解析的。本文你可以了解到以下内容:

  1. Go语法分析整体概览
  2. Go语法解析详细过程
  3. 示例展示语法解析完整过程

💡 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
  • appendGroup

首先是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()方法的源代码粘出来了。我这里梳理出来它主要干了哪些事情:

  1. 创建一个ImportDeal的结构体(最终会被append到File.DeclList中)
  2. 初始化结构体中的一些数据,比如解析到的token的位置信息、组(group)等
  3. 然后去匹配下一个token,看它是_Name的token类型(标识符),还是_Dot(.)
  4. 如果获取到的token是_Name,则获取到包名,如果获取到的是_Dot(.),则新建一个名字.
  5. 然后就是匹配包路径,主要是由oliteral()方法实现
  6. 返回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声明解析的内部实现。其实语法解析里边还有表达式解析、包括其他的一些语法的解析,内容比较多,没法一一介绍,感兴趣的小伙伴可以自行研究

感谢阅读,下一篇主题为:抽象语法树构建


书旅
125 声望32 粉丝