3B.3 数据结构

语法树相关的数据结构都在文件 $GCROOT/compile/internal/syntax/nodes.go 中,每个源文件对应一个如下结构体的实例:

// package PkgName; DeclList[0], DeclList[1], ...
type File struct {
    Pragma   Pragma // 编译指令,嵌入在源代码的注释中,形如:go://xxxx, 可以忽略不予考虑
    PkgName  *Name  // 该文件的包名
    DeclList []Decl // 该文件中包含的顶级声明
    EOF      Pos
    node     // 内嵌结构体,使 File 实现 nodes.go 中定义的 Node 接口
}

该结构是syntax.Parse()函数的返回结果,代表着一个源文件的 AST。其中重要的是 DeclList, 其包含了该源文件中所有的声明,声明(Declaration)是 Go 源文件顶级作用域中的唯一合法结构,声明在其内部递归地包含其他结构体。

该文件中定义的各种数据结构与 Go 语言规范 中所定义的语言结构是一一对应的,在源代码中,很多数据结构的声明上面都有对应的文法。所有的数据结构可以归为如下几类:

3.3.1 声明(Declaration)

声明就是在程序里面说“我要创建一个 X, 他的名字是 Y”,其包含两件事情:一个是命名,一个是绑定。例如 Go 中的 var name string 就是一个变量声明,其在程序中取了一个名字:name, 然后将其与 string 这种类型绑定起来,翻译成前边的语言就是:我要创建一个字符串,他的名字是 name。注意 name := "Golang" 不仅包含了声明,而且包含了赋值语句(Statement),只是声明中的类型信息是隐式的。

Go Spec 中对声明的阐述为:

A declaration binds a non-blank identifier to a constant, type, variable, function, label, or package. Every identifier in a program must be declared. No identifier may be declared twice in the same block, and no identifier may be declared in both the file and package block.

由此可见,声明其实就是创建程序组件并为其命名的操作,有了这些程序组件作为积木,我们才能搭建出整个程序大厦。在 Go 语言源文件中,合法的顶级声明包括:常量、类型、变量、函数、方法 以及 package, 下面都是合法的声明:

type A struct{}    // 声明一个结构体类型,其名字是 A
type B interface{} // 声明一个接口类型,其名字是 B

const a = 1 // 声明一个常量,将其命名为 a,并将其初始化为 1. 因为常量值无法修改,所以常量的声明必须与初始化在一条语句内完成。

var b string // 声明一个变量,将其命名为 b

func sum(i, j int) int // 声明一个函数,将其命名为 sum, 没有函数体。可以通过编译指令 `go:linkname` 将其与其他地方的函数链接起来
func sub(i, j int) int { // 声明一个函数,将其命名为 sub, 并包含函数体
    return i - j
}

type Str string        // 声明一个类型 Str, 其 underlying type 是 string
type StrAlias = string // 声明一个类型别名 StrAlias,其与 string 类型等价

在 nodes.go 中,每一种声明都有对应的数据结构,作为示例,我们在这里仅仅看一下函数声明的结构:

// func          Name Type { Body }
// func          Name Type
// func Receiver Name Type { Body }
// func Receiver Name Type
type FuncDecl struct {
    Pragma     Pragma
    Recv       *Field     // 如果该函数是一个方法,则表示 Receiver, 否则为 nil
    Name       *Name      // 函数名,是一个表达式(Expr)
    TParamList []*Field   // 类型参数,即泛型
    Type       *FuncType  // 函数类型,是一个表达式(Expr),其中包含函数的参数与返回值的列表
    Body       *BlockStmt // 函数体的语句(Statement),如果没有函数体则为空
    decl                  // 内嵌结构体,使该结构体实现接口 Decl
}

可以发现该结构体就是对文法的一种直接映射,其每个属性也代表了一个函数或者方法的某个代码块。

结构体 FuncDecl 可以表示函数、方法两类声明,实际上 Go 中函数与方法没有本质区别,方法与函数的区别在于方法的第一个参数默认绑定到调用的实例上,这与 Java 的 this 关键字是一样的。因为 Go 中函数是一类公民(First Class Citizen),所以我们甚至可以将方法赋值给变量,如下代码体现了方法与函数的等价性质:

package main

import "fmt"

type people struct {
    name string
}

func (this people) greeting(friend string) {
    fmt.Println("My name is ", this.name, ", How are you ", friend)
}

func main() {
    f := people.greeting
    p := people{"Golang"}

    f(p, "Java") // 与 f.greeting("Java") 效果一样
}

3.3.2 表达式(Expression)

表达式代表的是可以对其进行求值运算的代码块,例如算术运算、函数调用、访问数组或者哈希表等。

Go Spec 中对表达式的阐述为:

An expression specifies the computation of a value by applying operators and functions to operands.

也就是说表达式代表的是一个计算,该计算过程通过将某种运算符(operator)应用到操作数(operand)、或者将函数应用到其参数上来完成。常见的运算符包括算术运算符、逻辑运算符、位运算符等,我们对使用这类运算符做“计算”的认知是很自然的,例如表达式 i + j, 运算符(Operator)是 +, 操作数(Operand)是 ij. 但对于编译器而言,运算符的范围更加宽泛,例如赋值语句的右边只能是一个表达式,形如 a := b 中的 b 必须有值返回才能将其赋给 a. 例如如下代码:

language := struct {
    Name string
    Age  int
}{
    Name: "Golang",
    Age:  100,
}

赋值语句右侧以关键字 struct 开始,所以对于编译器而言 struct 就是一个运算符,其操作数就是紧跟着的 {} 所包围的代码块,编译器根据该运算符“计算”出了一个类型值,然后为该类型值初始化了一个实例赋值给 language.

nodes.go 中定义的表达式类型比较多,包括如下种类:

  1. 字面量 所有的字面量都通过一个表达式来表示:

    type BasicLit struct {
        Value string  // 字面量
        Kind  LitKind // 字面量类型,包括整数、浮点数、字符串等
        Bad   bool    // true means the literal Value has syntax errors
        expr
    }
  2. 数据访问 例如访问数组元素、访问 map、访问 struct 的字段等。形如 t[i] 通过下标访问数据的表达式定义如下:

    // X[Index]
    type IndexExpr struct {
        X     Expr // 被访问的对象,通过一个表达式来表示
        Index Expr // 下标,也通过一个表达式来表示
        expr
    }
  3. 类型定义 所有的类型定义都是表达式,这里我们仅仅看一下 struct 类型的表达式结构:

    // struct { FieldList[0] TagList[0]; FieldList[1] TagList[1]; ... }
    type StructType struct {
        FieldList []*Field    // 该 struct 内部所有字段的列表
        TagList   []*BasicLit // 每个字段的 tag, 通过下标与字段对应
        expr
    }

    类型表达式在类型检查时会用来构建具体的类型对象

这里只是简单列举了几类表达式,所有的表达式定义详见源文件。

3.3.3 语句(Statement)

如果说表达式是程序的计算单元的话,那么语句就是程序的控制单元。

Go Spec 中对语句的阐述为:

Statements control execution.

例如 for 控制循环,if…else… 控制分支选项等。我们看一下 if 语句的结构:

type IfStmt struct {
    Init SimpleStmt // if 语句的初始化语句,本身是一个语句
    Cond Expr       // 判断条件,一个返回值为 bool 的表达式
    Then *BlockStmt // 条件为 true 时执行的语句块
    Else Stmt       // 条件为假时执行的语句块,可以是 nil, *IfStmt, 或者 *BlockStmt 之一
    stmt
}

其他语句对应的结构体定义详见源文件。

3.3.4 其他

除了表示程序块的数据结构,该文件内还定义了一些辅助结构,包括:

  • 针对不同类型的几个接口,例如 Node, Decl, Expr, Stmt

  • Group

    // All declarations belonging to the same group point to the same Group node.
    type Group struct {
        _ int // not empty so we are guaranteed different Group instances
    }

    通过括号 () 括起来的一组声明都指向同一个 Group 实例。例如如下代码中所有的常量声明都会指向一个 Group 实例:

    const (
        name   = "Golang"
        friend = "Java"
    )

最后更新于