了解Go编译处理(四)—— go文件的解析

前言

了解Go编译处理(三)—— 初识go compile一文中,我们对接go compile的过程进行了初步的了解,从本篇起开始关注具体的处理部分。

本篇主要关注的是.go文件的解析——parseFiles

更多内容分享,欢迎关注公众号:Go开发笔记

parseFiles的调用

func Main(archInit func(*Arch)) {
...
    timings.Start("fe", "parse")
    lines := parseFiles(flag.Args())
    timings.Stop()
...
}

parseFiles是在gc.Main正式进行语法检查前进行的。

parseFiles

parseFiles负责具体的.go文件解析。

// parseFiles concurrently parses files into *syntax.File structures.
// Each declaration in every *syntax.File is converted to a syntax tree
// and its root represented by *Node is appended to xtop.
// Returns the total count of parsed lines.
func parseFiles(filenames []string) uint {
    noders := make([]*noder, 0, len(filenames))
    // Limit the number of simultaneously open files.
    sem := make(chan struct{}, runtime.GOMAXPROCS(0)+10)

    for _, filename := range filenames {
        p := &noder{
            basemap: make(map[*syntax.PosBase]*src.PosBase),
            err:     make(chan syntax.Error),
        }
        noders = append(noders, p)

        go func(filename string) {
            sem <- struct{}{}
            defer func() { <-sem }()
            defer close(p.err)
            base := syntax.NewFileBase(filename)

            f, err := os.Open(filename)
            if err != nil {
                p.error(syntax.Error{Pos: syntax.MakePos(base, 0, 0), Msg: err.Error()})
                return
            }
            defer f.Close()

            p.file, _ = syntax.Parse(base, f, p.error, p.pragma, syntax.CheckBranches) // errors are tracked via p.error
        }(filename)
    }

    var lines uint
    for _, p := range noders {
        for e := range p.err {
            p.yyerrorpos(e.Pos, "%s", e.Msg)
        }

        p.node()
        lines += p.file.Lines
        p.file = nil // release memory

        if nsyntaxerrors != 0 {
            errorexit()
        }
        // Always run testdclstack here, even when debug_dclstack is not set, as a sanity measure.
        testdclstack()
    }

    localpkg.Height = myheight

    return lines
}

文件解析采用并发形式进行的,可以加快解析的速度,为了避免过多的文件打开占用资源,此处限制了打开文件的数量为设置的最大CPU核数+10

解析的结果通过带缓存的channel noders进行传递,缓存大小为len(filenames),足以同时存储所有的解析结果。

通过noders读取解析结果,若正常则构造node,然后释放资源,期间存在语法错误则会报错并退出,删除已生成文件。

具体的解析处理在syntax.Parse中进行。

Parse

先根据注释了解下Parse的处理。

Parse负责解析单个.go文件,然后返回对应的语法树。如果有错误,Parse将返回发现的第一个错误,以及可能部分构造的语法树,或者nil。
如果errh != nil,则在遇到每个错误时调用它,Parse将处理尽可能多的源代码。在这种情况下,仅当找不到正确的package子句时,返回的语法树才为nil。
如果errh为nil,则遇到第一个错误时,解析将立即终止,并且返回的语法树为nil。
如果pragh!= nil,则在遇到每个编译指示时都会调用它。

// Parse parses a single Go source file from src and returns the corresponding
// syntax tree. If there are errors, Parse will return the first error found,
// and a possibly partially constructed syntax tree, or nil.
//
// If errh != nil, it is called with each error encountered, and Parse will
// process as much source as possible. In this case, the returned syntax tree
// is only nil if no correct package clause was found.
// If errh is nil, Parse will terminate immediately upon encountering the first
// error, and the returned syntax tree is nil.
//
// If pragh != nil, it is called with each pragma encountered.
//
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()
    return p.fileOrNil(), p.first
}

Parse分为两步,init中将文件存入parser的src,next中负责对src中的内容一个个rune的进行解析。

p.init

p.init负责参数及scanner的初始化及error的处理func

func (p *parser) init(file *PosBase, r io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) {
    p.file = file
    p.errh = errh
    p.mode = mode
    p.scanner.init(
        r,
        // Error and directive handler for scanner.
        // Because the (line, col) positions passed to the
        // handler is always at or after the current reading
        // position, it is safe to use the most recent position
        // base to compute the corresponding Pos value.
        func(line, col uint, msg string) {
            if msg[0] != '/' {
                p.errorAt(p.posAt(line, col), msg)
                return
            }

            // otherwise it must be a comment containing a line or go: directive
            text := commentText(msg)
            if strings.HasPrefix(text, "line ") {
                var pos Pos // position immediately following the comment
                if msg[1] == '/' {
                    // line comment (newline is part of the comment)
                    pos = MakePos(p.file, line+1, colbase)
                } else {
                    // regular comment
                    // (if the comment spans multiple lines it's not
                    // a valid line directive and will be discarded
                    // by updateBase)
                    pos = MakePos(p.file, line, col+uint(len(msg)))
                }
                p.updateBase(pos, line, col+2+5, text[5:]) // +2 to skip over // or /*
                return
            }

            // go: directive (but be conservative and test)
            if pragh != nil && strings.HasPrefix(text, "go:") {
                p.pragma |= pragh(p.posAt(line, col+2), text) // +2 to skip over // or /*
            }
        },
        directives,
    )

    p.base = file
    p.first = nil
    p.errcnt = 0
    p.pragma = 0

    p.fnest = 0
    p.xnest = 0
    p.indent = nil
}

p.next

p.next负责依次对满足条件的rune进行解析,解析结束后会记录解析处的文件、行数、列数的信息。对于解析错误的语句提供具体的位置信息,交由init中的errorh负责。

注意:单次p.next只能获取满足的条件的rune,一个完整语句可能要调用多次p.next。对于一个go文件,最先解析到的信息,应该是package信息。

func (s *scanner) next() {
    nlsemi := s.nlsemi
    s.nlsemi = false

redo:
    // skip white space
    c := s.getr()//获取下一个rune
    for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' {//如果是空格、tab、换行、回车,则重新获取下一个
        c = s.getr()
    }

    // token start
    // 记录行与列数
    s.line, s.col = s.source.line0, s.source.col0
    // 关键字、变量名(以字母或下划线开头,不能以数字开头)的处理,如package、var、type、等
    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 '`':// raw字符串的引号
        s.rawString()

    case '\'': // rune
        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) { // 如果是float
            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
}

扫描的过程是一个个rune进行的。如果是空格、tab、换行、回车,会直接忽略,重新获取下一个rune;如果是数字、字母、下划线(即变量名或关键字)等,则获取完整的名称。特殊的操作符则添加标识说明。最后在fileOrNil中继续获取下一个rune,直到文件结束。

扫描的过程中包含了我们熟悉的各种的符合的处理和标记。

正常情况下,第一次p.next获取到的应该是package,如果不是说明.go文件内容有问题。

在next还可以看到,在换行、文件等结束会默认添加分号;,所以fileOrNil中会在语法的结束时校验_Semi

fileOrNil

.go文件的内容基本格式:

package package_name

import "import_package_1"
import name2 "import_package_2"
// or
import (
    "import_package_3"
   name4  "import_package_4"
)
var variables ...
var (
    ...
)
type defined_types ...
type (
    ...
)
func functions ...

fileOrNil就是是从文件开头检查整个文件,首先检查package及import,然后循环检查顶级声明(const、type、var、func),直至文件结束(或发生错误)。

具体解析时,循环查找时,确认关键字,然后根据是否存在’(’,确定是否是多个值的组合,进行单个或多个值的解析。func还会对func body内部进行解析。

// SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .
func (p *parser) fileOrNil() *File {
    if trace {
        defer p.trace("file")()
    }

    f := new(File)
    f.pos = p.pos()

    // PackageClause
    // go文件从package开始,因此先检查package
    if !p.got(_Package) {// 检查并获取package name
        p.syntaxError("package statement must be first")
        return nil
    }
    // 获取package名称
    f.PkgName = p.name()
    p.want(_Semi) // 检查';'

    // don't bother continuing if package clause has errors
    if p.first != nil {
        return nil
    }

    // { ImportDecl ";" }
    // import的处理
    for p.got(_Import) {
        f.DeclList = p.appendGroup(f.DeclList, p.importDecl)
        p.want(_Semi)
    }

    // { TopLevelDecl ";" }
    // 顶级声明的获取,包含const、type、var、func
    // 将获取到的const、type、var、func的name、value等参数封装后存入声明列表DeclList
    for p.tok != _EOF {// 一直到文件结束
        switch p.tok {
        case _Const:// const
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.constDecl)

        case _Type: // type
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.typeDecl)

        case _Var: // var
            p.next()
            f.DeclList = p.appendGroup(f.DeclList, p.varDecl)

        case _Func: // 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
}

总结

Parse负责对具体文件进行解析,根据.go文件的格式要求,需要先检查package,imports,然后循环获取顶级声明的const、type、var、func(func会进一步解析func body)并解析封装在对应的Decl中,最后存入DeclList数组中,供解析的下一步node的构建做数据的准备。

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

你可能感兴趣的:(golang,源码分析)