• 深入挖掘分析Go代码


    微信公众号:[double12gzh]

    关注容器技术、关注Kubernetes。问题或建议,请公众号留言。

    写在前面

    本文基于GoLang 1.14

    在语法层面对源代码进行分析,可以通过多种方式帮助你进行编码。为此,几乎总是先将文本转换成AST,以便在大多数语言中更容易处理。
    可能有些人知道,Go有一个强大的包go/parser,有了它,你可以比较容易地将源代码转换为AST。然而,我不禁对它的工作原理充满了好奇,我意识到只有开始阅读API的实现才能满足我的好奇心。

    在本文中,我将通过阅读它的API实现,带大家了解它是如何转换的。

    即使是对Go语言不熟悉的人,也建议可以看一下本文,因为这是一篇足够通用的文章,通读本文,您可以了解编程语言的分析方法。
    这篇文章也是了解编译器和解释器的第一步,同时也是深入研究静态分析的第一步。

    AST

    先说说阅读实现所需要的一些知识吧。什么是AST(Abstract Syntax Tree)?根据维基百科的介绍。

    在计算机科学中,抽象语法树(AST),或者仅仅是语法树,是用编程语言编写的源代码的抽象语法结构的树状表示。树的每个节点都表示源代码中出现的一个构造。
    大多数编译器和解释器都使用AST作为源代码的内部表示,AST通常会省略语法树中的分号、换行字符、白空格、大括号、方括号和圆括号等。

    用AST可以做什么?

    • 源代码分析
    • 代码生成
    • 可改写

    如何生成AST

    纯文本对我们来说是很直接的,但从机器角度来看,应该是没有什么比这更难处理的了。因此,你必须先用一个词法分析器对文本进行词法分析。一般的流程是把它传给一个解析器,然后检索AST。

    我想在这里指出,没有一种通用的AST格式可以被任何解析器使用。例如,在GoLang中,x+2用以下格式表示:

    *ast.BinaryExpr {
    .  X: *ast.Ident {
    .  .  NamePos: 1
    .  .  Name: "x"
    .  .  Obj: *ast.Object {
    .  .  .  Kind: bad
    .  .  .  Name: ""
    .  .  }
    .  }
    .  OpPos: 3
    .  Op: +
    .  Y: *ast.BasicLit {
    .  .  ValuePos: 5
    .  .  Kind: INT
    .  .  Value: "2"
    .  }
    }
    

    词法分析

    如前所述,分析通常从将文本传递给词典开始,然后获取tokentoken是一个带有分配并有识别意义的字符串。go/scanner.Scanner负责Go中的lexer。
    识别的意义是什么?接着往下看。

    比方说你写了下面的这段代码:

    package main
    
    const s = "foo"
    

    下面就是你把它标记化后的结果:

    PACKAGE(package)
    IDENT(main)
    CONST(const)
    IDENT(s)
    ASSIGN(=)
    STRING("foo")
    EOF()
    

    GoLang中所支持的所有的token请参考token package

    揭开解析API的面纱

    为了将GoLang代码生成其对应的AST,我们可以简单的调用:go/parser.ParseFile ,例如:

    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "foo.go", nil, parser.ParseComments)
    

    通过前一篇文章的学习,我们已经搞清楚了转换的步骤,那么我们就来实际阅读一下该方法的内部实现吧!

    Scanner.Scan()

    这是词法分析的一种方法,那么,Go如何进行词法分析呢?

    如前所述,go/scanner.Scanner负责Go中的词法分析器。因此,首先让我们仔细看看那个Scanner.Scan()方法(它是由parser.ParseFile()内部调用的)。

    scanner/scanner.go

    func (s *Scanner) Scan() (pos token.Pos, tok token.Token, lit string) {
    ... // Omission
    	switch ch := s.ch; {
    	case isLetter(ch):
    		lit = s.scanIdentifier()
    		if len(lit) > 1 {
    			// keywords are longer than one letter - avoid lookup otherwise
    			tok = token.Lookup(lit)
    			switch tok {
    			case token.IDENT, token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN:
    				insertSemi = true
    			}
    		} else {
    ... // Omission
    }
    

    ch是当前由Scanner持有的字符。Scanner.Scan()通过调用Scanner.next()前进到下一个字符,只要它可以作为标识符名称,就会并填充ch
    上面的代码是针对ch是字母的情况,一旦遇到不能作为标识符的字符,它就会暂停前进,然后确定标识符的类型。

    根据字符的不同,有不同的方法来确定单个令牌的起点和终点。例如,在String的情况下,它会继续前进,直到出现""。

    scanner/scanner.go

    case '"':
    	insertSemi = true
    	tok = token.STRING
        lit = s.scanString()
    
    func (s *Scanner) scanString() string {
    	// '"' opening already consumed
    	offs := s.offset - 1
    
    	for {
    		ch := s.ch
    		if ch == '
    ' || ch < 0 {
    			s.error(offs, "string literal not terminated")
    			break
    		}
    		s.next()
    		if ch == '"' {
    			break
    		}
    		if ch == '\' {
    			s.scanEscape('"')
    		}
    	}
    
    	return string(s.src[offs:s.offset])
    }
    

    最后,Scanner.Scan()将会返回一个被认证过的token

    分析

    在看解析文件之前,我们先来看看Go中的文件结构。根据《Go编程语言规范》:每个源文件都由一个包子句组成,定义它所属的包,后面是一组可能是空的导入声明,声明它想使用的包的内容,后面是一组可能是空的函数、类型、变量和常量的声明。

    也就是说,其结构是:

    • 一个包子句
    • 导入声明
    • 顶层声明

    在解析完包子句和导入声明后,parser.parseFile()会重复解析声明到文件的最后。

    parser/parser.go

    for p.tok != token.EOF {
    	decls = append(decls, p.parseDecl(declStart))
    }
    

    下面我们看一下parser.parseDecl

    parser.parseDecl()解析声明语法的方法,返回ast.Decl,即代表 Go 源代码中声明的语法树的节点。

    parser/parser.go

    func (p *parser) parseDecl(sync map[token.Token]bool) ast.Decl {
    	if p.trace {
    		defer un(trace(p, "Declaration"))
    	}
    
    	var f parseSpecFunction
    	switch p.tok {
    	case token.CONST, token.VAR:
    		f = p.parseValueSpec
    
    	case token.TYPE:
    		f = p.parseTypeSpec
    
    	case token.FUNC:
    		return p.parseFuncDecl()
    
    	default:
    		pos := p.pos
    		p.errorExpected(pos, "declaration")
    		p.advance(sync)
    		return &ast.BadDecl{From: pos, To: p.pos}
    	}
    
    	return p.parseGenDecl(p.tok, f)
    }
    

    它通过token,对每个关键字进行不同的处理。让我们深入了解一下parseFuncDecl()。

    parser/parser.go

    if p.tok == token.LPAREN {
    	recv = p.parseParameters(scope, false)
    }
    
    ident := p.parseIdent()
    
    params, results := p.parseSignature(scope)
    
    var body *ast.BlockStmt
    if p.tok == token.LBRACE {
    	body = p.parseBody(scope)
    	p.expectSemi()
    } else if p.tok == token.SEMICOLON {
        p.next()
    

    在内部,它通过调用Scanner.Scan()来处理token(我们在前面已经详细看过了)。

    • token.LPAREN代表(,所以你可以看到一旦找到(,它就开始解析参数。
    • token.LBRACE代表{,所以你可以看到一旦找到{,它就开始解析函数主体。

    写在后面

    通过上面的分析,让我觉得自己和以前看似陌生的编译器和解释器更接近了,后面我也在想要不要写一个《在Go中写一个编译器》和《在Go中写一个解释器》。

  • 相关阅读:
    模拟实现atoi函数
    C语言中的字符串函数的实现,strcpy,strlen,strcat,strcmp
    使用repeater实现gridview的功能
    使用NPOI随意创建Excel(含下拉列表)
    使用存储过程来动态调用数据(SELECT)
    判断sql执行效率以及针对临时表的使用
    C#读取Excel显示到repeater中
    ASP.NET使用后台更改前台Style
    js格式化日期
    查询某张表的表结构
  • 原文地址:https://www.cnblogs.com/double12gzh/p/13644840.html
Copyright © 2020-2023  润新知