一、Go 的編譯器在邏輯上能夠被分紅四個階段:詞法與語法分析、類型檢查和 AST 轉換、通用 SSA 生成和最後的機器代碼生成前端
編譯前端 編譯後端 ---------- | ---------- | | 詞法分析 | 中間碼生成 | |---------------------- | | 語法分析 | 代碼優化 | |---------------------- | | 語義分析 | 機器碼生成 | |--------- | ---------- |
詞法分析是計算機科學中將字符序列轉換爲標記(token)序列的過程,詞法分析就是解析源代碼文件,它將文件中的字符串序列轉換成Token序列,方便後面的處理和解析。
語法分析的輸入就是詞法分析器輸出的 Token 序列,這些序列會按照順序被語法分析器進行解析,語法分析過程就是將詞法分析生成的 Token 按照語言定義好的文法(Grammar)自下而上或者自上而下的進行規約,每個 Go 的源代碼文件最終會被概括成一個SourceFile結構。
詞法分析會返回一個不包含空格、換行等字符的 Token 序列,例如:package
, import
, (
, io
, )
, …,而語法分析會把 Token 序列轉換成有意義的結構體,也就是抽象語法樹(AST)。
每個 AST 都對應着一個單獨的 Go 語言文件,這個抽象語法樹中包括當前文件屬於的包名、定義的常量、結構體和函數等。node
當拿到一組文件的抽象語法樹以後,Go 語言的編譯器會對語法樹中定義和使用的類型進行檢查,類型檢查分別會按照如下的順序對不一樣類型的節點進行驗證和處理:golang
經過對整棵抽象語法樹的遍歷,咱們在每個節點上都會對當前子樹的類型進行驗證,以保證當前節點上不會出現類型錯誤的問題,全部的類型錯誤和不匹配都會在這一個階段被發現和暴露出來,結構體是否實現了某些接口也會在這一階段被檢查出來。express
當咱們將源文件轉換成了抽象語法樹、對整棵樹的語法進行解析並進行類型檢查以後,就能夠認爲當前文件中的代碼不存在語法錯誤和類型錯誤的問題了,Go 語言的編譯器就會將輸入的抽象語法樹轉換成中間代碼。
在類型檢查以後,就會經過一個名爲 compileFunctions
的函數開始對整個 Go 語言項目中的所有函數進行編譯,這些函數會在一個編譯隊列中等待幾個後端工做協程的消費,這些併發執行的 Goroutine 會將全部函數對應的抽象語法樹轉換成中間代碼。
因爲 Go 語言編譯器的中間代碼使用了 SSA 的特性,因此在這一階段咱們就可以分析出代碼中的無用變量和片斷並對代碼進行優化。後端
Go 語言源代碼的 src/cmd/compile/internal
目錄中包含了不少機器碼生成相關的包,不一樣類型的 CPU 分別使用了不一樣的包生成機器碼,其中包括 amd6四、arm、arm6四、mips、mips6四、ppc6四、s390x、x86 和 wasm。數組
官網地址: https://golang.google.cn/dl/ 下載安裝包或源碼包都可 wget https://golang.google.cn/dl/go1.15.4.src.tar.gz 安裝包下載很簡單 設置好環境變量。 源碼安裝 cd go/src ./all.bash 安裝報錯 # ./all.bash ./make.bash: line 165: /root/go1.4/bin/go: No such file or directory Building Go cmd/dist using /root/go1.4. () ERROR: Cannot find /root/go1.4/bin/go. Set $GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.
GOROOT_BOOTSTRAP 設置go1.4版本地址 實現了自舉,編譯後續版本
咱們學習的對象就是Go語言版本的編譯器程序源碼,源碼目錄go/src/cmd/compile
。bash
調試編譯器執行過程,必須調試go安裝目錄下的編譯器源碼
,即GOROOT下源碼,不然報內部包衝突錯誤。
編譯器目錄爲go/src/cmd/compile,調試命令以下:閉包
#一樣必須是GOROOT下源碼 dlv debug /usr/local/go/src/cmd/compile/main.go #設置斷點 經過包名及方法設置斷點 b main.main #設置編譯器編譯目標文件 r /Users/xxx/go/src/test/debug1/main.go #開始調試 continue c #執行下一行代碼 n # 打印關心的變量 p archInit cmd/compile/internal/amd64.Init # 經過代碼文件及行數設置斷點 b /usr/local/go/src/cmd/compile/internal/gc/main.go:345 p outfile #直接經過函數名設置斷點 b parseFiles b funccompile
同理 goland調試編譯器 調試的源碼也必須位於go安裝目錄
首先須要經過Goland新建項目go 項目目錄位於GOROOT,例如/user/local/go
Goland Debug配置方法以下 和dlv相似併發
打開 /usr/local/go/src/cmd/compile/main.go 點擊main函數左側綠色小三角形,執行 debug go build main.go (未設置則自動)彈出go build配置框或點擊右上方Edit Debug Configurations 菜單按鈕從新設置 Files /usr/local/go/src/cmd/compile/main.go # 設置有權限的工做目錄 Working Directory /Users/xxx/golang/compile/w # 設置編譯目標文件 Program arguments /Users/xxx/go/src/test/debug1/main.go
以上是Goland debug的關鍵配置,具體使用方法,自行百度,和普通程序debug調試用法一致。app
經過main函數,不難追蹤到文件解析入口函數parseFiles,該函數爲每一個文件建立了一個noder對象,noders := make([]*noder, 0, len(filenames))
noder擁有一個詞法文件屬性存儲該源文件對應語法分析的結果。file *syntax.File //詞法分析文件對象指針
單個源文件對應的語法樹noder
結構體,noder
結構體以下
// noder transforms package syntax's AST into a Node tree. type noder struct { basemap map[*syntax.PosBase]*src.PosBase basecache struct { last *syntax.PosBase base *src.PosBase } file *syntax.File //詞法語法分析文件對象指針 linknames []linkname pragcgobuf [][]string err chan syntax.Error scope ScopeID // scopeVars is a stack tracking the number of variables declared in the // current function at the moment each open scope was opened. scopeVars []int lastCloseScopePos syntax.Pos }
syntax.File詞法分析生成結果定義以下:
// package PkgName; DeclList[0], DeclList[1], ... type File struct { PkgName *Name DeclList []Decl Lines uint node }
main
入口經過parseFiles
方法開啓協程經過syntax.Parse
進行文件解析.
Parse函數代碼以下:
func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) { defer func() { if p := recover(); p != nil { if err, ok := p.(Error); ok { first = err return } panic(p) } }() var p parser //聲名語法分析樹 p.init(base, src, errh, pragh, mode) p.next() //開始識別token return p.fileOrNil(), p.first //p.fileOrNil()開始生成token詞法樹 }
解析過程當中,建立了語法分析器parser,var p parser //聲名語法分析樹
其中繼承了詞法分析器 sanner
的方法和屬性,parser定義以下:
#解析器結構體 type parser struct { file *PosBase errh ErrorHandler mode Mode scanner //繼承掃描器scanner的方法屬性 base *PosBase // current position base first error // first error encountered errcnt int // number of errors encountered pragma Pragma // pragma flags fnest int // function nesting level (for error handling) xnest int // expression nesting level (for complit ambiguity resolution) indent []byte // tracing support }
詞法分析器scanner結構體定義以下:
#詞法分析掃描器結構體 type scanner struct { source //持有源文件對象 mode uint nlsemi bool // if set 'n' and EOF translate to ';' // current token, valid after calling next() line, col uint tok token lit string // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true bad bool // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed kind LitKind // valid if tok is _Literal op Operator // valid if tok is _Operator, _AssignOp, or _IncOp prec int // valid if tok is _Operator, _AssignOp, or _IncOp }
源文件對象定義以下:
type source struct { src io.Reader errh func(line, pos uint, msg string) // source buffer buf [4 << 10]byte r0, r, w int // previous/current read and write buf positions, excluding sentinel line0, line uint // previous/current line col0, col uint // previous/current column (byte offsets from line start) ioerr error // pending io error // literal buffer lit []byte // literal prefix suf int // literal suffix; suf >= 0 means we are scanning a literal }
詞法分析函數(syntax.Parse
)的 fileOrNil()
方法返回文件結構體syntax.File
,也就是解析完畢的詞法分析詞法結果樹。
Go 語言的詞法解析是經過src/cmd/compile/internal/syntax/scanner.go文件中的 scanner
結構體實現的,這個結構體(見上文定義
)會持有當前掃描的數據源文件、啓用的模式和當前被掃描到的Token。src/cmd/compile/internal/syntax/tokens.go
文件中定義了 Go 語言中支持的所有 Token 類型,全部的 token
類型都是正整數,你能夠在這個文件中找到一些常見 Token 的定義,例如:操做符、括號和關鍵字等:
const ( _ token = iota _EOF // EOF // names and literals _Name // name _Literal // literal // operators and operations // _Operator is excluding '*' (_Star) _Operator // op _AssignOp // op= _IncOp // opop _Assign // = _Define // := _Arrow // <- _Star // * // delimiters _Lparen // ( _Lbrack // [ _Lbrace // { _Rparen // ) _Rbrack // ] _Rbrace // } _Comma // , _Semi // ; _Colon // : _Dot // . _DotDotDot // ... // keywords _Break // break _Case // case _Chan // chan _Const // const _Continue // continue _Default // default _Defer // defer _Else // else _Fallthrough // fallthrough _For // for _Func // func _Go // go _Goto // goto _If // if _Import // import _Interface // interface _Map // map _Package // package _Range // range _Return // return _Select // select _Struct // struct _Switch // switch _Type // type _Var // var // empty line comment to exclude it from .String tokenCount // )
最右推導加向前查看構成了Go語言詞法分析器的最基本原理,主要是由 scanner
這個結構體中的 next
方法驅動來獲取下一個詞法token,方法代碼以下:
func (s *scanner) next() { nlsemi := s.nlsemi s.nlsemi = false redo: // skip white space c := s.getr() //過濾空字符,獲取下一個字符 for c == ' ' || c == 't' || c == 'n' && !nlsemi || c == 'r' { c = s.getr() } // token start s.line, s.col = s.source.line0, s.source.col0 if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) { s.ident() return } switch c { case -1: if nlsemi { s.lit = "EOF" s.tok = _Semi break } s.tok = _EOF case 'n': s.lit = "newline" s.tok = _Semi case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': s.number(c) case '"': s.stdString() case '`': s.rawString() case ''': s.rune() case '(': s.tok = _Lparen case '[': s.tok = _Lbrack case '{': s.tok = _Lbrace case ',': s.tok = _Comma case ';': s.lit = "semicolon" s.tok = _Semi case ')': s.nlsemi = true s.tok = _Rparen case ']': s.nlsemi = true s.tok = _Rbrack case '}': s.nlsemi = true s.tok = _Rbrace case ':': if s.getr() == '=' { s.tok = _Define break } s.ungetr() s.tok = _Colon case '.': c = s.getr() if isDecimal(c) { s.ungetr() s.unread(1) // correct position of '.' (needed by startLit in number) s.number('.') break } if c == '.' { c = s.getr() if c == '.' { s.tok = _DotDotDot break } s.unread(1) } s.ungetr() s.tok = _Dot case '+': s.op, s.prec = Add, precAdd c = s.getr() if c != '+' { goto assignop } s.nlsemi = true s.tok = _IncOp case '-': s.op, s.prec = Sub, precAdd c = s.getr() if c != '-' { goto assignop } s.nlsemi = true s.tok = _IncOp case '*': s.op, s.prec = Mul, precMul // don't goto assignop - want _Star token if s.getr() == '=' { s.tok = _AssignOp break } s.ungetr() s.tok = _Star case '/': c = s.getr() if c == '/' { s.lineComment() goto redo } if c == '*' { s.fullComment() if s.source.line > s.line && nlsemi { // A multi-line comment acts like a newline; // it translates to a ';' if nlsemi is set. s.lit = "newline" s.tok = _Semi break } goto redo } s.op, s.prec = Div, precMul goto assignop case '%': s.op, s.prec = Rem, precMul c = s.getr() goto assignop case '&': c = s.getr() if c == '&' { s.op, s.prec = AndAnd, precAndAnd s.tok = _Operator break } s.op, s.prec = And, precMul if c == '^' { s.op = AndNot c = s.getr() } goto assignop case '|': c = s.getr() if c == '|' { s.op, s.prec = OrOr, precOrOr s.tok = _Operator break } s.op, s.prec = Or, precAdd goto assignop case '^': s.op, s.prec = Xor, precAdd c = s.getr() goto assignop case '<': c = s.getr() if c == '=' { s.op, s.prec = Leq, precCmp s.tok = _Operator break } if c == '<' { s.op, s.prec = Shl, precMul c = s.getr() goto assignop } if c == '-' { s.tok = _Arrow break } s.ungetr() s.op, s.prec = Lss, precCmp s.tok = _Operator case '>': c = s.getr() if c == '=' { s.op, s.prec = Geq, precCmp s.tok = _Operator break } if c == '>' { s.op, s.prec = Shr, precMul c = s.getr() goto assignop } s.ungetr() s.op, s.prec = Gtr, precCmp s.tok = _Operator case '=': if s.getr() == '=' { s.op, s.prec = Eql, precCmp s.tok = _Operator break } s.ungetr() s.tok = _Assign case '!': if s.getr() == '=' { s.op, s.prec = Neq, precCmp s.tok = _Operator break } s.ungetr() s.op, s.prec = Not, 0 s.tok = _Operator default: s.tok = 0 s.errorf("invalid character %#U", c) goto redo } return assignop: if c == '=' { s.tok = _AssignOp return } s.ungetr() s.tok = _Operator }
經過getr獲取下一個字符,經過許多不一樣的switch/case來識別token。
而經過解析器(parser)的fileOrNil方法來將識別的token加入到語法樹:
func (p *parser) fileOrNil() *File { if trace { defer p.trace("file")() } f := new(File) f.pos = p.pos() // PackageClause if !p.got(_Package) { p.syntaxError("package statement must be first") return nil } f.PkgName = p.name() p.want(_Semi) // don't bother continuing if package clause has errors if p.first != nil { return nil } // { ImportDecl ";" } for p.got(_Import) { f.DeclList = p.appendGroup(f.DeclList, p.importDecl) p.want(_Semi) } // { TopLevelDecl ";" } for p.tok != _EOF { switch p.tok { case _Const: p.next() f.DeclList = p.appendGroup(f.DeclList, p.constDecl) case _Type: p.next() f.DeclList = p.appendGroup(f.DeclList, p.typeDecl) case _Var: p.next() f.DeclList = p.appendGroup(f.DeclList, p.varDecl) case _Func: p.next() if d := p.funcDeclOrNil(); d != nil { f.DeclList = append(f.DeclList, d) } default: if p.tok == _Lbrace && len(f.DeclList) > 0 && isEmptyFuncDecl(f.DeclList[len(f.DeclList)-1]) { // opening { of function declaration on next line p.syntaxError("unexpected semicolon or newline before {") } else { p.syntaxError("non-declaration statement outside function body") } p.advance(_Const, _Type, _Var, _Func) continue } // Reset p.pragma BEFORE advancing to the next token (consuming ';') // since comments before may set pragmas for the next function decl. p.pragma = 0 if p.tok != _EOF && !p.got(_Semi) { p.syntaxError("after top level declaration") p.advance(_Const, _Type, _Var, _Func) } } // p.tok == _EOF f.Lines = p.source.line return f }
經過fileOrNil方法,不難看出go語言四種不一樣的頂級聲明token(TopLevelDecl)詞法節點:_Const(常量)、_Type(類型)、_Var(變量)、_Func(函數及方法)以及_Package和_Import聲明token。
解析器還封裝了p.want()和p.got(),進行前驅分析和意圖判斷。
func (p *parser) got(tok token) bool { if p.tok == tok { p.next()//若是當前tok和指定token相等,返回true並自動前驅獲取下個token return true } return false } func (p *parser) want(tok token) { if !p.got(tok) {//若是不符合預期則報語法錯誤。 p.syntaxError("expecting " + tokstring(tok)) p.advance() } }
got
其實只是用於快速判斷一些語句中的關鍵字,若是當前解析器中的 Token 是傳入的 Token 就會直接跳過該 Token 並返回 true
;而 want
就是對 got
的簡單封裝了,若是當前的 Token 不是咱們指望的,就會馬上返回語法錯誤並結束此次編譯。
// 頂級聲明 type ( Decl interface { Node aDecl() } // Path // LocalPkgName Path ImportDecl struct { LocalPkgName *Name // including "."; nil means no rename present Path *BasicLit Group *Group // nil means not part of a group decl } // NameList // NameList = Values // NameList Type = Values ConstDecl struct { NameList []*Name Type Expr // nil means no type Values Expr // nil means no values Group *Group // nil means not part of a group decl } // Name Type TypeDecl struct { Name *Name Alias bool Type Expr Group *Group // nil means not part of a group Pragma Pragma decl } // NameList Type // NameList Type = Values // NameList = Values VarDecl struct { NameList []*Name Type Expr // nil means no type Values Expr // nil means no values Group *Group // nil means not part of a group decl } // func Name Type { Body } // func Name Type // func Receiver Name Type { Body } // func Receiver Name Type FuncDecl struct { Attr map[string]bool // go:attr map Recv *Field // nil means regular function Name *Name Type *FuncType Body *BlockStmt // nil means no body (forward declaration) Pragma Pragma // TODO(mdempsky): Cleaner solution. decl } )
以FuncDecl爲例,函數體存儲類型爲BlockStmt指針,常見statments定義以下:
// Statements type ( Stmt interface { Node aStmt() } SimpleStmt interface { Stmt aSimpleStmt() } EmptyStmt struct { simpleStmt } LabeledStmt struct { Label *Name Stmt Stmt stmt } BlockStmt struct { List []Stmt Rbrace Pos stmt } ExprStmt struct { X Expr simpleStmt } 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 } BranchStmt struct { Tok token // Break, Continue, Fallthrough, or Goto Label *Name // Target is the continuation of the control flow after executing // the branch; it is computed by the parser if CheckBranches is set. // Target is a *LabeledStmt for gotos, and a *SwitchStmt, *SelectStmt, // or *ForStmt for breaks and continues, depending on the context of // the branch. Target is not set for fallthroughs. Target Stmt stmt } CallStmt struct { Tok token // Go or Defer Call *CallExpr stmt } 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 } SwitchStmt struct { Init SimpleStmt Tag Expr // incl. *TypeSwitchGuard Body []*CaseClause Rbrace Pos stmt } SelectStmt struct { Body []*CommClause Rbrace Pos stmt } )
這些不一樣類型的 Stmt
構成了所有命令式的 Go 語言代碼,從中咱們能夠看到很是多熟悉的控制結構,例如 if
、for
、switch
和 select
。
由於詞法分析器 scanner
做爲結構體被嵌入到了 parser
中,因此這個方法中的 p.next()
實際上調用的是 scanner
的 next
方法,它會直接獲取文件中的下一個 Token,因此詞法分析和語法分析實際上是一塊兒進行的。
上面parseFiles方法中詞法掃描器和語法解析器(syntax.Parse)生成源文件對應的syntax.File對象後,生成抽象語法樹的過程是經過p.decls()方法生成的xtop = append(xtop, p.decls(p.file.DeclList)...)
xtop是go語言抽象語法樹數組var xtop []*Node
。
func (p *noder) node() { types.Block = 1 imported_unsafe = false p.setlineno(p.file.PkgName) mkpackage(p.file.PkgName.Value) xtop = append(xtop, p.decls(p.file.DeclList)...)//生成抽象語法樹 for _, n := range p.linknames { if !imported_unsafe { p.yyerrorpos(n.pos, "//go:linkname only allowed in Go files that import "unsafe"") continue } s := lookup(n.local) if n.remote != "" { s.Linkname = n.remote } else { // Use the default object symbol name if the // user didn't provide one. if myimportpath == "" { p.yyerrorpos(n.pos, "//go:linkname requires linkname argument or -p compiler flag") } else { s.Linkname = objabi.PathToPrefix(myimportpath) + "." + n.local } } } // The linker expects an ABI0 wrapper for all cgo-exported // functions. for _, prag := range p.pragcgobuf { switch prag[0] { case "cgo_export_static", "cgo_export_dynamic": if symabiRefs == nil { symabiRefs = make(map[string]obj.ABI) } symabiRefs[prag[1]] = obj.ABI0 } } pragcgobuf = append(pragcgobuf, p.pragcgobuf...) lineno = src.NoXPos clearImports() } //xtop聲明以下: var xtop []*Node
語法樹單個樹節點定義以下:
// A Node is a single node in the syntax tree. // Actually the syntax tree is a syntax DAG, because there is only one // node with Op=ONAME for a given instance of a variable x. // The same is true for Op=OTYPE and Op=OLITERAL. See Node.mayBeShared. type Node struct { // Tree structure. // Generic recursive walks should follow these fields. Left *Node Right *Node Ninit Nodes Nbody Nodes List Nodes Rlist Nodes // most nodes Type *types.Type Orig *Node // original form, for printing, and tracking copies of ONAMEs // func Func *Func // ONAME, OTYPE, OPACK, OLABEL, some OLITERAL Name *Name Sym *types.Sym // various E interface{} // Opt or Val, see methods below // Various. Usually an offset into a struct. For example: // - ONAME nodes that refer to local variables use it to identify their stack frame position. // - ODOT, ODOTPTR, and ORESULT use it to indicate offset relative to their base address. // - OSTRUCTKEY uses it to store the named field's offset. // - Named OLITERALs use it to store their ambient iota value. // - OINLMARK stores an index into the inlTree data structure. // - OCLOSURE uses it to store ambient iota value, if any. // Possibly still more uses. If you find any, document them. Xoffset int64 Pos src.XPos flags bitset32 Esc uint16 // EscXXX Op Op aux uint8 }
下來咱們來經過Goland IDE來debug下golang編譯器
golang程序源碼以下
package main import "fmt" type st struct { name string age int } type St st var ( avg int32 = 123 tag string = "aaa" //ttt int64 ) const ( A = 111 B = 222 ) func Echo(t string, f interface{}) { fmt.Printf(t, f) } func init() { } func main() { a := [...]int{1, 2, 3} Echo("a:%+v", a) }
斷點設置go/src/cmd/compile/internal/gc/noder.go:246
行
運行至端點後鼠標浮於xtop
變量之上,點擊+號浮框展現變量內容:
展開xtop變量觀察語法樹數組結構以下:
上圖展現的是xtop[0]和xtop[2]及部分xtop[8]
瞭解 Go 語言的詞法分析器 scanner
和語法分析器 parser
讓咱們對解析器處理源代碼的過程有着比較清楚的認識,同時咱們也在 Go 語言的文法和語法分析器中找到了熟悉的關鍵字和語法結構,加深了對 Go 語言的理解。