Go学习笔记-Go编译器简介

1.编译器

1.1 三阶段编译器

  • 编译器前端: 主要用于理解源代码、扫描解析源代码并进行语义表达
  • IR: Intermediate Representation,可能有多个,编译器会使用多个 IR 阶段、多种数据结构表示程序,并在中间阶段对代码进行多次优化
  • 优化器: 主要目的是降低程序资源的消耗,但有理论已经表明某些优化存在着NP难题,所以编译器无法进行最佳优化,通常常用折中方案
  • 编译后端: 主要用于生成特定目标机器上的程序,可能是可执行文件,也可能是需要进一步处理的obj、汇编语言等

1.2 Go语言编译器

  • go compiler: Go语言编译器的主要源代码位于"src/cmd/compile/internal"目录下

Tips: 注意:大写的GC表示垃圾回收

2.词法解析

_IncOp":="_Definego/src/cmd/compile/internal/syntax/tokens.go
go/src/go/scannergo/src/go/token

2.1 scanner.go 代码简介

go/src/go/scanner[]byteScan
  • type ErrorHandler func(pos token.Position, msg string): 可以向Scanner.Init 提供 ErrorHandler,如果遇到语法错误并安装了处理程序,则调用处理程序并提供位置和错误消息,该位置指向错误标记的开始。
  • type Scanner struct: 扫描器在处理给定文本时保持扫描器的内部状态。
  • const bom = 0xFEFF: 字节顺序标记,只允许作为第一个字符。
  • next()函数: 将下一个Unicode字符读入 s.ch,S.ch < 0 表示文件结束。
  • peek()函数: Peek返回跟随最近读取字符的字节,而不推进扫描仪,如果扫描器在 EOF, peek 返回 0。
  • type Mode uint: 模式值是一组标志(或 0),它们控制扫描行为。
const (
	ScanComments    Mode = 1 << iota // 返回注释作为注释标记
	dontInsertSemis                  // 不要自动插入分号-仅用于测试
)
  • Init()函数: Init 通过在 src 的开头设置扫描器来让扫描器 s 标记文本src,扫描器使用文件集文件来获取位置信息,并为每一行添加行信息。当重新扫描相同的文件时,可以重复使用相同的文件,因为已经存在的行信息被忽略了。如果文件大小与 src 大小不匹配,Init 会导致 panic。如果遇到语法错误且 err 不是nil,则调用 Scan 将调用错误处理程序 err。此外,对于遇到的每个错误, Scanner 字段 ErrorCount 增加 1,mode 参数决定如何处理注释。

Tips: 注意,如果文件的第一个字符有错误,Init 可能会调用 err。

a-zA-Z_utf8.RuneSelf
func isLetter(ch rune) bool {
	return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch)
}
0-9utf8.RuneSelf
func isDigit(ch rune) bool {
	return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch)
}
  • digitVal()函数: 返回 rune 字符对应的整型数。
  • scanEscape()函数: scanEscape 解析转义序列,其中 rune 是接受的转义引号,在语法错误的情况下,它在错误字符处停止(不使用它)并返回 false,否则返回 true。

2.2 token.go 代码代码简介

go/src/go/token
  • init()函数: 初始化关键字keywords变量(map[string]Token)的值
map[break:61 case:62 chan:63 const:64 continue:65 default:66 defer:67 else:68 fallthrough:69 for:70 func:71 go:72 goto:73 if:74 import:75 interface:76 map:77 package:78 range:79 return:80 select:81 struct:82 switch:83 type:84 var:85]
  • String()函数: 返回与令牌 tok 对应的字符串,对于操作符、分隔符和关键字,字符串是实际的标记字符序列(例如,对于标记ADD,字符串是“+”),对于所有其他令牌,字符串对应于令牌常量名(例如,对于令牌IDENT,字符串为“IDENT”)
  • Precedence()函数: 返回二进制操作符 op 的操作符优先级,如果op不是二进制操作符,则结果为最低优先级
  • Lookup()函数: 查找标记符对应的token
  • IsLiteral() 函数: 判断token值是否是基本类型范围的值,若是返回true,否则返回false
literal_beg
	// Identifiers and basic type literals
	// (these tokens stand for classes of literals)
	IDENT  // main
	INT    // 12345
	FLOAT  // 123.45
	IMAG   // 123.45i
	CHAR   // 'a'
	STRING // "abc"
	literal_end
  • IsOperator() 函数: 判断token值是否是操作范围的值,若是返回true,否则返回false
operator_beg
	// Operators and delimiters
	ADD // +
	SUB // -
	MUL // *
	QUO // /
	REM // %

	AND     // &
	OR      // |
	XOR     // ^
	SHL     // <<
	SHR     // >>
	AND_NOT // &^

	ADD_ASSIGN // +=
	SUB_ASSIGN // -=
	MUL_ASSIGN // *=
	QUO_ASSIGN // /=
	REM_ASSIGN // %=

	AND_ASSIGN     // &=
	OR_ASSIGN      // |=
	XOR_ASSIGN     // ^=
	SHL_ASSIGN     // <<=
	SHR_ASSIGN     // >>=
	AND_NOT_ASSIGN // &^=

	LAND  // &&
	LOR   // ||
	ARROW // <-
	INC   // ++
	DEC   // --

	EQL    // ==
	LSS    // <
	GTR    // >
	ASSIGN // =
	NOT    // !

	NEQ      // !=
	LEQ      // <=
	GEQ      // >=
	DEFINE   // :=
	ELLIPSIS // ...

	LPAREN // (
	LBRACK // [
	LBRACE // {
	COMMA  // ,
	PERIOD // .

	RPAREN    // )
	RBRACK    // ]
	RBRACE    // }
	SEMICOLON // ;
	COLON     // :
	operator_end
  • IsKeyword() 函数: 判断token值是否是关键字范围的值,若是返回true,否则返回false
keyword_beg
	// Keywords
	BREAK
	CASE
	CHAN
	CONST
	CONTINUE

	DEFAULT
	DEFER
	ELSE
	FALLTHROUGH
	FOR

	FUNC
	GO
	GOTO
	IF
	IMPORT

	INTERFACE
	MAP
	PACKAGE
	RANGE
	RETURN

	SELECT
	STRUCT
	SWITCH
	TYPE
	VAR
	keyword_end

3.语法解析

go/src/cmd/compile/internal/syntax/nodes.gogo/src/cmd/compile/internal/syntax/parser.go
//包导入声明
ImportSpec = [ "." | PackageName ] ImportPath .
ImportPath = string_lit .
//静态常量
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
//类型声明
TypeSpec = identifier [ "=" ] Type .
//变量声明
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

3.1 nodes.go 代码代码简介

  • 函数声明:
AssignStmt struct {
	Op       Operator // 表示当前的操作符,即":=",0 表示没有操作
		Lhs, Rhs Expr     // Lhs, Rhs 分别代表左右两个表达式,Rhs == ImplicitOne means Lhs++ (Op == Add) or Lhs-- (Op == Sub)
		simpleStmt
	}
  • type Node interface:
type Node interface {
    //Pos() 返回与节点相关联的位置,如下所示:
	// 1)表示终端语法产物(Name,BasicLit等)的节点位置是相应产物在源中的位置。
	// 2)表示非终端产品(IndexExpr, IfStmt等)的节点的位置是与该产品唯一关联的令牌的位置;通常是最左边的一个('['表示IndexExpr, 'if'表示IfStmt,等等)
	Pos() Pos
	aNode()
}
包 PkgName; DeclList[0], DeclList[1], ...

3.2 parser.go 代码代码简介

  • importDecl()函数: 包导入声明。
  • constDecl()函数: 静态常量。
  • typeDecl()函数: 类型声明。
  • varDecl()函数: 变量声明。
  • type parser struct:
type parser struct {
	file *PosBase
	errh ErrorHandler
	mode Mode
	scanner

	base   *PosBase // 当前位置的基准
	first  error    // 遇到的第一个错误
	errcnt int      // 遇到的错误数
	pragma Pragma   // 编译指示标志

	fnest  int    // 函数嵌套级别(用于错误处理)
	xnest  int    // 表达式嵌套级别(用于解决编译歧义)
	indent []byte //跟踪支持
}
  • updateBase()函数: updateBase 将当前的位置基设置为 pos 处的新行的基准,该行基准的文件名、行和列值从定位于 (tline, tcol) 的文本中提取(仅在错误消息中需要)。
  • posAt()函数: posAt返回 (line, col) 的 Pos 值和当前位置的基数。
  • errorAt()函数: 错误报告在给定位置的错误。
  • syntaxErrorAt()函数: syntaxErrorAt 在给定位置报告语法错误。
  • tokstring()函数: tokstring 为更可读的错误消息返回所选标点符号的英文单词。
  • const stopset uint64: stopset 包含启动语句的关键字。在语法错误的情况下,它们是很好的同步点,(通常)不应该跳过。
const stopset uint64 = 1<<_Break |
	1<<_Const |
	1<<_Continue |
	1<<_Defer |
	1<<_Fallthrough |
	1<<_For |
	1<<_Go |
	1<<_Goto |
	1<<_If |
	1<<_Return |
	1<<_Select |
	1<<_Switch |
	1<<_Type |
	1<<_Var
  • advance()函数: Advance 消耗令牌,直到找到 stopset 或 followlist 的令牌。只有在函数 (p.fnest > 0) 中才考虑 stopset,如果它是空的,则只使用一个 (非eof) 令牌来确保进度。
  • fileOrNil()函数: 包文件 Parse 方法会根据需要使用匹配的 Go 结果进行注释,注释的目的只是作为引导,因为单个 Go 语法规则可能被多个解析方法覆盖,排除返回切片的方法,名为 xOrNil 的解析方法可能返回 nil,所有其他节点都将返回一个有效的非 nil 节点。
  • list()函数: List 解析一个可能为空的、由 sep 分隔的列表,可选后跟 sep 并由 (and) 或 {and} 括起来。open 是 Lparen 的一个,sep 是 Comma 或 Semi 的一个对于每个列表元素,调用 fmf 返回 true 后,不再接受列表元素,List 返回结束令牌的位置。

3.3 语法解析举例

a := b + c(10)
Tips: c(10) 表示强类型转换。
a := b + c(10)

4.抽象语法树构建

go/src/cmd/compile/internal/gc/noder.gogo/src/cmd/compile/internal/gc/syntax.go
func (p *noder) decls(decls []syntax.Decl) (l []*Node) {
	var cs constState

	for _, decl := range decls {
		p.setlineno(decl)
		switch decl := decl.(type) {
		case *syntax.ImportDecl:
			p.importDecl(decl)

		case *syntax.VarDecl:
			l = append(l, p.varDecl(decl)...)

		case *syntax.ConstDecl:
			l = append(l, p.constDecl(decl, &cs)...)

		case *syntax.TypeDecl:
			l = append(l, p.typeDecl(decl))

		case *syntax.FuncDecl:
			l = append(l, p.funcDecl(decl))

		default:
			panic("unhandled Decl")
		}
	}

	return
}

5.类型检查

var a intgo/src/cmd/compile/internal/gc/typecheck.go

6.变量捕获

go/src/cmd/compile/internal/gc/closure.gocapturevars
package main

import (
	"fmt"
)

func main()  {
	a := "qinshixian"
	b := make(map[string]int)
	c := "haoweilai"
	go func() {
		fmt.Println(a)
		fmt.Println(b)
		fmt.Println(c)
	}()
	a = "asddss"
}
go tool compile -m=2 getVar.go| grep capturing
arefbcvalue

7.函数内联

go/src/cmd/compile/internal/gc/inl.goforrangegoselect
package main

import "testing"

//go:noinline
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

var Result int

func BenchmarkMax(b *testing.B) {
    var r int
    for i := 0; i < b.N; i++ {
        r = max(-1, i)
    }
    Result = r
}
//go:noinline
//go:noinlinego test leetcode_test.go -bench=.
//go:noinlinego test leetcode_test.go -bench=.
forrangegoselect
go tool compile -m=2 leetcode_test.go

8.逃逸分析

go/src/cmd/compile/internal/gc/escape .go
(1)原则1:指向栈上对象的指针不能被存储到堆中
(2)原则2:指向栈上对象的指针不能超过该栈对象的生命周期
  • 逃逸现象举例:
package main

var n *int

func escape(){
	a := 100
	n = &a
}

func main()  {
	escape()
}
go tool compile -m getVar.go
nintanaa