我们拿到一个golang的工程后(通常是个微服务),怎么从词法、语法的角度来分析源代码呢?golang提供了一系列的工具供我们使用:
go/scanner包提供词法分析功能,将源代码转换为一系列的token,以供go/parser使用
go/parser包提供语法分析功能,将这些token转换为AST(Abstract Syntax Tree, 抽象语法树)
Scanner
任何编译器所做的第一步都是将源代码转换成token,这就是Scanner所做的事
token可以是关键字,字符串值,变量名以及函数名等等
在golang中,每个token都以它所处的位置,类型和原始字面量来表示
https://www.jianshu.com/p/937d649039ec
AST的结构定义
go/ast/ast.go中指明了ast节点的定义:
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
语法有三个主体:表达式(expression)、语句(statement)、声明(declaration),Node是基类,用于标记该节点的位置的开始和结束。而三个主体的函数没有实际意义,只是用三个interface来划分不同的语法单位,如果某个语法是Stmt的话,就实现一个空的stmtNode函数即可。
比如我们用如下代码扫描源代码的token:
func TestScanner(t *testing.T) {
src := []byte(package main
)
import "fmt"
//comment
func main() {
fmt.Println("Hello, world!")
}
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
s.Init(file, src, nil, 0)
for {
pos, tok, lit := s.Scan()
fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit)
if tok == token.EOF {
break
}
} } 结果:
1:1 package “package”
1:9 IDENT “main”
1:13 ; “\n”
2:1 import “import”
2:8 STRING “"fmt"”
2:13 ; “\n”
4:1 func “func”
4:6 IDENT “main”
4:10 ( “”
4:11 ) “”
4:13 { “”
5:3 IDENT “fmt”
5:6 . “”
5:7 IDENT “Println”
5:14 ( “”
5:15 STRING “"Hello, world!"”
5:30 ) “”
5:31 ; “\n”
6:1 } “”
6:2 ; “\n”
6:3 EOF “”
注意没有扫描出注释,需要的话要将s.Init的最后一个参数改为scanner.ScanComments。
看下go/token/token.go的源代码可知,token就是一堆定义好的枚举类型,对于每种类型的字面值都有对应的token。
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package token defines constants representing the lexical tokens of the Go
// programming language and basic operations on tokens (printing, predicates).
//
package token
import “strconv”
// Token is the set of lexical tokens of the Go programming language.
type Token int
// The list of tokens.
const (
// Special tokens
ILLEGAL Token = iota
EOF
COMMENT
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
...略... ) Parser 当源码被扫描成token之后,结果就被传递给了Parser 将token转换为抽象语法树(AST) 编译时的错误也是在这个时候报告的 什么是AST呢,这篇文章何为语法树讲的很好。简单来说,AST(Abstract Syntax Tree)是使用树状结构表示源代码的语法结构,树的每一个节点就代表源代码中的一个结构。
来看如下的例子:
func TestParserAST(t *testing.T) {
src := []byte(/*comment0*/
)
package main
import "fmt"
//comment1
/*comment2*/
func main() {
fmt.Println("Hello, world!")
}
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f) } 结果很长就不贴出来了,整个AST的树形结构可以用如下图表示:
image
同样注意没有扫描出注释,需要的话要将parser.ParseFile的最后一个参数改为parser.ParseComments。再对照如下ast.File的定义:
type File struct {
Doc CommentGroup // associated documentation; or nil
Package token.Pos // position of “package” keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []ImportSpec // imports in this file
Unresolved []Ident // unresolved identifiers in this file
Comments []CommentGroup // list of all comments in the source file
}
可知上述例子中的/comment0/对照结构中的Doc,是整个go文件的描述。和//comment1以及/comment2/不同,后两者是Decls中的结构。
遍历AST
golang提供了ast.Inspect方法供我们遍历整个AST树,比如如下例子遍历整个example/test1.go文件寻找所有return返回的地方:
func TestInspectAST(t *testing.T) {
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, “./example/test1.go”, nil, parser.ParseComments)
if err != nil {
panic(err)
}
ast.Inspect(f, func(n ast.Node) bool {
// Find Return Statements
ret, ok := n.(*ast.ReturnStmt)
if ok {
fmt.Printf("return statement found on line %v:\n", fset.Position(ret.Pos()))
printer.Fprint(os.Stdout, fset, ret)
fmt.Printf("\n")
return true
}
return true
}) } example/test1.go代码如下:
package main
import “fmt”
import “strings”
func test1() {
hello := “Hello”
world := “World”
words := []string{hello, world}
SayHello(words)
}
// SayHello says Hello
func SayHello(words []string) bool {
fmt.Println(joinStrings(words))
return true
}
// joinStrings joins strings
func joinStrings(words []string) string {
return strings.Join(words, “, “)
}
结果为:
return statement found on line ./example/test1.go:16:2:
return true
return statement found on line ./example/test1.go:21:2:
return strings.Join(words, “, “)
还有另一种方法遍历AST,构造一个ast.Visitor接口:
type Visitor int
func (v Visitor) Visit(n ast.Node) ast.Visitor {
if n == nil {
return nil
}
fmt.Printf(“%s%T\n”, strings.Repeat(“\t”, int(v)), n)
return v + 1
}
func TestASTWalk(t *testing.T) {
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, “”, “package main; var a = 3”, parser.ParseComments)
if err != nil {
panic(err)
}
var v Visitor
ast.Walk(v, f)
}
旨在递归地打印出所有的token节点,输出:
*ast.File
*ast.Ident
*ast.GenDecl
*ast.ValueSpec
*ast.Ident
*ast.BasicLit
以上基础知识主要参考文章How a Go Program Compiles down to Machine Code(译文:Go 程序到机器码的编译之旅)。下面来点干货。
怎么找到特定的代码块
其实翻一翻网上将这个golang的ast的文章也不少,但是大多停留在上文的阶段,没有实际指导开发运用。那么我们假设现在有一个任务,拿到了一个别人的项目(俗称接盘侠),现在需要找到源文件中的这些地方:特征是调用了context.WithCancel函数,并且入参为nil。比如example/test2.go文件里面,有十多种可能:
package main
import (
“context”
“fmt”
)
func test2(a string, b int) {
context.WithCancel(nil) //000
if _, err := context.WithCancel(nil); err != nil { //111
context.WithCancel(nil) //222
} else {
context.WithCancel(nil) //333
}
_, _ = context.WithCancel(nil) //444
go context.WithCancel(nil) //555
go func() {
context.WithCancel(nil) //666
}()
defer context.WithCancel(nil) //777
defer func() {
context.WithCancel(nil) //888
}()
data := map[string]interface{}{
"x2": context.WithValue(nil, "k", "v"), //999
}
fmt.Println(data)
/*
for i := context.WithCancel(nil); i; i = false {//aaa
context.WithCancel(nil)//bbb
}
*/
var keys []string = []string{"ccc"}
for _, k := range keys {
fmt.Println(k)
context.WithCancel(nil)
} } 从000到ccc,对应golang的AST的不同结构类型,现在需要把他们全部找出来。其中bbb这种情况代表了for语句,只不过在context.WithCancel函数不适用,所以注掉了。为了解决这个问题,首先需要仔细分析go/ast的Node接口。
AST的结构定义
go/ast/ast.go中指明了ast节点的定义:
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
语法有三个主体:表达式(expression)、语句(statement)、声明(declaration),Node是基类,用于标记该节点的位置的开始和结束。而三个主体的函数没有实际意义,只是用三个interface来划分不同的语法单位,如果某个语法是Stmt的话,就实现一个空的stmtNode函数即可。参考这篇文章go-parser-语法分析,定义了源文件中可能出现的语法结构。列表如下:
普通Node,不是特定语法结构,属于某个语法结构的一部分.
Comment 表示一行注释 // 或者 / /
CommentGroup 表示多行注释
Field 表示结构体中的一个定义或者变量,或者函数签名当中的参数或者返回值
FieldList 表示以”{}”或者”()”包围的Filed列表
Expression & Types (都划分成Expr接口)
BadExpr 用来表示错误表达式的占位符
Ident 比如报名,函数名,变量名
Ellipsis 省略号表达式,比如参数列表的最后一个可以写成arg…
BasicLit 基本字面值,数字或者字符串
FuncLit 函数定义
CompositeLit 构造类型,比如{1,2,3,4}
ParenExpr 括号表达式,被括号包裹的表达式
SelectorExpr 选择结构,类似于a.b的结构
IndexExpr 下标结构,类似这样的结构 expr[expr]
SliceExpr 切片表达式,类似这样 expr[low:mid:high]
TypeAssertExpr 类型断言类似于 X.(type)
CallExpr 调用类型,类似于 expr()
StarExpr 表达式,类似于 X
UnaryExpr 一元表达式
BinaryExpr 二元表达式
KeyValueExp 键值表达式 key:value
ArrayType 数组类型
StructType 结构体类型
FuncType 函数类型
InterfaceType 接口类型
MapType map类型
ChanType 管道类型
Statements
BadStmt 错误的语句
DeclStmt 在语句列表里的申明
EmptyStmt 空语句
LabeledStmt 标签语句类似于 indent:stmt
ExprStmt 包含单独的表达式语句
SendStmt chan发送语句
IncDecStmt 自增或者自减语句
AssignStmt 赋值语句
GoStmt Go语句
DeferStmt 延迟语句
ReturnStmt return 语句
BranchStmt 分支语句 例如break continue
BlockStmt 块语句 {} 包裹
IfStmt If 语句
CaseClause case 语句
SwitchStmt switch 语句
TypeSwitchStmt 类型switch 语句 switch x:=y.(type)
CommClause 发送或者接受的case语句,类似于 case x <-:
SelectStmt select 语句
ForStmt for 语句
RangeStmt range 语句
Declarations
Spec type
Import Spec
Value Spec
Type Spec
BadDecl 错误申明
GenDecl 一般申明(和Spec相关,比如 import “a”,var a,type a)
FuncDecl 函数申明
Files and Packages
File 代表一个源文件节点,包含了顶级元素.
Package 代表一个包,包含了很多文件.
全类型匹配
那么我们需要仔细判断上面的总总结构,来适配我们的特征:
package go_code_analysis
import (
“fmt”
“go/ast”
“go/token”
“log”
)
var GFset *token.FileSet
var GFixedFunc map[string]Fixed //key的格式为Package.Func
func stmtCase(stmt ast.Stmt, todo func(call ast.CallExpr) bool) bool {
switch t := stmt.(type) {
case *ast.ExprStmt:
log.Printf(“表达式语句%+v at line:%v”, t, GFset.Position(t.Pos()))
if call, ok := t.X.(ast.CallExpr); ok {
return todo(call)
}
case ast.ReturnStmt:
for i, p := range t.Results {
log.Printf(“return语句%d:%v at line:%v”, i, p, GFset.Position(p.Pos()))
if call, ok := p.(ast.CallExpr); ok {
return todo(call)
}
}
case ast.AssignStmt:
//函数体里的构造类型 999
for _, p := range t.Rhs {
switch t := p.(type) {
case *ast.CompositeLit:
for i, p := range t.Elts {
switch t := p.(type) {
case *ast.KeyValueExpr:
log.Printf(“构造赋值语句%d:%+v at line:%v”, i, t.Value, GFset.Position(p.Pos()))
if call, ok := t.Value.(ast.CallExpr); ok {
return todo(call)
}
}
}
}
}
default:
log.Printf(“不匹配的类型:%T”, stmt)
}
return false
}
//调用函数的N种情况
//对函数调用使用todo适配,并返回是否适配成功
func AllCallCase(n ast.Node, todo func(call *ast.CallExpr) bool) (find bool) {
//函数体里的直接调用 000
if fn, ok := n.(*ast.FuncDecl); ok {
for i, p := range fn.Body.List {
log.Printf("函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
find = find || stmtCase(p, todo)
}
log.Printf("func:%+v done", fn.Name.Name)
}
//if语句里
if ifstmt, ok := n.(*ast.IfStmt); ok {
log.Printf("if语句开始:%T %+v", ifstmt, GFset.Position(ifstmt.If))
//if的赋值表达式 111
if a, ok := ifstmt.Init.(*ast.AssignStmt); ok {
for i, p := range a.Rhs {
log.Printf("if语句赋值%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
switch call := p.(type) {
case *ast.CallExpr:
c := todo(call)
find = find || c
}
}
}
//if的花括号里面 222
for i, p := range ifstmt.Body.List {
log.Printf("if语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
//if的else里面 333
if b, ok := ifstmt.Else.(*ast.BlockStmt); ok {
for i, p := range b.List {
log.Printf("if语句else表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
}
log.Printf("if语句结束:%+v done", GFset.Position(ifstmt.End()))
}
//赋值语句 444
if assign, ok := n.(*ast.AssignStmt); ok {
log.Printf("赋值语句开始:%T %s", assign, GFset.Position(assign.Pos()))
for i, p := range assign.Rhs {
log.Printf("赋值表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
switch t := p.(type) {
case *ast.CallExpr:
c := todo(t)
find = find || c
case *ast.CompositeLit:
for i, p := range t.Elts {
switch t := p.(type) {
case *ast.KeyValueExpr:
log.Printf("构造赋值%d:%+v at line:%v", i, t.Value, GFset.Position(p.Pos()))
if call, ok := t.Value.(*ast.CallExpr); ok {
c := todo(call)
find = find || c
}
}
}
}
}
}
if gostmt, ok := n.(*ast.GoStmt); ok {
log.Printf("go语句开始:%T %s", gostmt.Call.Fun, GFset.Position(gostmt.Go))
//go后面直接调用 555
c := todo(gostmt.Call)
find = find || c
//go func里面的调用 666
if g, ok := gostmt.Call.Fun.(*ast.FuncLit); ok {
for i, p := range g.Body.List {
log.Printf("go语句表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
}
log.Printf("go语句结束:%+v done", GFset.Position(gostmt.Go))
}
if deferstmt, ok := n.(*ast.DeferStmt); ok {
log.Printf("defer语句开始:%T %s", deferstmt.Call.Fun, GFset.Position(deferstmt.Defer))
//defer后面直接调用 777
c := todo(deferstmt.Call)
find = find || c
//defer func里面的调用 888
if g, ok := deferstmt.Call.Fun.(*ast.FuncLit); ok {
for i, p := range g.Body.List {
log.Printf("defer语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
}
log.Printf("defer语句结束:%+v done", GFset.Position(deferstmt.Defer))
}
if fostmt, ok := n.(*ast.ForStmt); ok {
//for语句对应aaa和bbb
log.Printf("for语句开始:%T %s", fostmt.Body, GFset.Position(fostmt.Pos()))
for i, p := range fostmt.Body.List {
log.Printf("for语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
}
if rangestmt, ok := n.(*ast.RangeStmt); ok {
//range语句对应ccc
log.Printf("range语句开始:%T %s", rangestmt.Body, GFset.Position(rangestmt.Pos()))
for i, p := range rangestmt.Body.List {
log.Printf("range语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
c := stmtCase(p, todo)
find = find || c
}
}
return }
type FindContext struct {
File string
Package string
LocalFunc *ast.FuncDecl
}
func (f *FindContext) Visit(n ast.Node) ast.Visitor {
if n == nil {
return f
}
if fn, ok := n.(*ast.FuncDecl); ok {
log.Printf("函数[%s.%s]开始 at line:%v", f.Package, fn.Name.Name, GFset.Position(fn.Pos()))
f.LocalFunc = fn
} else {
log.Printf("类型%T at line:%v", n, GFset.Position(n.Pos()))
}
find := AllCallCase(n, f.FindCallFunc)
if find {
name := fmt.Sprintf("%s.%s", f.Package, f.LocalFunc.Name)
GFixedFunc[name] = Fixed{FuncDesc: FuncDesc{f.File, f.Package, f.LocalFunc.Name.Name}}
}
return f }
func (f *FindContext) FindCallFunc(call *ast.CallExpr) bool {
if call == nil {
return false
}
log.Printf("call func:%+v, %v", call.Fun, call.Args)
if callFunc, ok := call.Fun.(*ast.SelectorExpr); ok {
if fmt.Sprint(callFunc.X) == "context" && fmt.Sprint(callFunc.Sel) == "WithCancel" {
if len(call.Args) > 0 {
if argu, ok := call.Args[0].(*ast.Ident); ok {
log.Printf("argu type:%T, %s", argu.Name, argu.String())
if argu.Name == "nil" {
location := fmt.Sprint(GFset.Position(argu.NamePos))
log.Printf("找到关键函数:%s.%s at line:%v", callFunc.X, callFunc.Sel, location)
return true
}
}
}
}
}
return false } 在AllCallCase方法中我们穷举了所有的调用函数的情况(ast.CallExpr),分别对应了000到ccc这13种情况。stmtCase方法分析了语句的各种可能,尽量找全所有。 FindContext.FindCallFunc方法首先看调用函数是不是选择结构,类似于a.b的结构;然后对比了调用函数的a.b是不是我们关心的context.WithCancel;最后看第一个实参的名称是不是nil。
最终找到了所有特征点:
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:9:21
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:11:34
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:12:22
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:14:22
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:11:34
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:17:28
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:19:24
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:22:22
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:25:27
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:28:22
2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:45:22
故事的结尾,我们使用FindContext提供的walk方法递归了AST树,找到了所有符合我们特征的函数,当然例子里就test一个函数。所有代码都在https://github.com/baixiaoustc/go_code_analysis中能找到。
https://github.com/baixiaoustc/go_code_analysis
https://getstream.io/blog/how-a-go-program-compiles-down-to-machine-code/
https://getstream.io/blog/switched-python-go/
https://github.com/golang/go/blob/3fd364988ce5dcf3aa1d4eb945d233455db30af6/src/cmd/compile/internal/ssa/gen/genericOps.go#L411
https://studygolang.com/articles/15648?utm_source=tuicool&utm_medium=referral
https://studygolang.com/articles/6709
本文主要看一下Go的语法分析是如何进行.Go的parser接受的输入是源文件,内嵌了一个scanner,最后把scanner生成的token变成一颗抽象语法树(AST).
编译时的错误也是在这个时候报告的,但是大部分编译器编译时的错误系统并不是很完美,有时候报的错误文不对题,这主要是因为写对的方式有几种
但是写错的方式有很多种,编译器只能把一些错误进行归类,并且指出当前认为可疑的地方,并不能完完全全的知道到底是什么语法错误.这个需要结合给出的错误进行判断,clang作为一个C编译器做得好很多,这都是开发者不断地添加错误处理的结果,比gcc的报错完善很多.然而Go的编译时的错误处理也是秉承了gcc的风格,并不明确,但是会指出可疑的地方,在大多数场景下或者对语言标准熟悉的情况下也不是很麻烦.
下面看一下Go是怎么定义这些语法结构.这些结构都在go/ast当中.
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
语法有三个主体,表达式(expression),语句(statement),声明(declaration),Node是基类,用于标记该节点的位置的开始和结束.
而三个主体的函数没有实际意义,只是用三个interface来划分不同的语法单位,如果某个语法是Stmt的话,就实现一个空的stmtNode函数即可.
这样的好处是可以对语法单元进行comma,ok来判断类型,并且保证只有这些变量可以赋值给对应的interface.但是实际上这个划分不是很严格,比如
func (ArrayType) exprNode() {}
func (StructType) exprNode() {}
func (FuncType) exprNode() {}
func (InterfaceType) exprNode() {}
就是类型,但是属于Expr,而真正的表达式比如
func (BasicLit) exprNode() {}
func (FuncLit) exprNode() {}
是可以赋值给Exprt的.
了解了这个设计,再来看整个内容其实就是定义了源文件中可能出现的语法结构.列表如下,这个列表很长,扫一眼就可以,具体可以再回来看.
普通Node,不是特定语法结构,属于某个语法结构的一部分.
Comment 表示一行注释 // 或者 / /
CommentGroup 表示多行注释
Field 表示结构体中的一个定义或者变量,或者函数签名当中的参数或者返回值
FieldList 表示以”{}”或者”()”包围的Filed列表
Expression & Types (都划分成Expr接口)
BadExpr 用来表示错误表达式的占位符
Ident 比如报名,函数名,变量名
Ellipsis 省略号表达式,比如参数列表的最后一个可以写成arg…
BasicLit 基本字面值,数字或者字符串
FuncLit 函数定义
CompositeLit 构造类型,比如{1,2,3,4}
ParenExpr 括号表达式,被括号包裹的表达式
SelectorExpr 选择结构,类似于a.b的结构
IndexExpr 下标结构,类似这样的结构 expr[expr]
SliceExpr 切片表达式,类似这样 expr[low:mid:high]
TypeAssertExpr 类型断言类似于 X.(type)
CallExpr 调用类型,类似于 expr()
StarExpr 表达式,类似于 X
UnaryExpr 一元表达式
BinaryExpr 二元表达式
KeyValueExp 键值表达式 key:value
ArrayType 数组类型
StructType 结构体类型
FuncType 函数类型
InterfaceType 接口类型
MapType map类型
ChanType 管道类型
Statements
BadStmt 错误的语句
DeclStmt 在语句列表里的申明
EmptyStmt 空语句
LabeledStmt 标签语句类似于 indent:stmt
ExprStmt 包含单独的表达式语句
SendStmt chan发送语句
IncDecStmt 自增或者自减语句
AssignStmt 赋值语句
GoStmt Go语句
DeferStmt 延迟语句
ReturnStmt return 语句
BranchStmt 分支语句 例如break continue
BlockStmt 块语句 {} 包裹
IfStmt If 语句
CaseClause case 语句
SwitchStmt switch 语句
TypeSwitchStmt 类型switch 语句 switch x:=y.(type)
CommClause 发送或者接受的case语句,类似于 case x <-:
SelectStmt select 语句
ForStmt for 语句
RangeStmt range 语句
Declarations
Spec type
Import Spec
Value Spec
Type Spec
BadDecl 错误申明
GenDecl 一般申明(和Spec相关,比如 import “a”,var a,type a)
FuncDecl 函数申明
Files and Packages
File 代表一个源文件节点,包含了顶级元素.
Package 代表一个包,包含了很多文件.
上面就是整个源代码的所有组成元素,接下来就来看一下语法分析是如何进行的,也就是最后的AST是如何构建出来的.
先看一下parser结构体的定义,parser是以file为单位的.
// The parser structure holds the parser’s internal state.
type parser struct {
file *token.File
errors scanner.ErrorList // 解析过程中遇到的错误列表
scanner scanner.Scanner // 词法分析器.
// Tracing/debugging
mode Mode // parsing mode // 解析模式
trace bool // == (mode & Trace != 0)
indent int // indentation used for tracing output
// Comments 列表
comments []*ast.CommentGroup
leadComment *ast.CommentGroup // last lead comment
lineComment *ast.CommentGroup // last line comment
// Next token
pos token.Pos // token position
tok token.Token // one token look-ahead
lit string // token literal
// Error recovery
// (used to limit the number of calls to syncXXX functions
// w/o making scanning progress - avoids potential endless
// loops across multiple parser functions during error recovery)
syncPos token.Pos // last synchronization position 解析错误的同步点.
syncCnt int // number of calls to syncXXX without progress
// Non-syntactic parser control
// 非语法性的控制
// <0 在控制语句中, >= 在表达式中.
exprLev int // < 0: in control clause, >= 0: in expression
// 正在解析右值表达式
inRhs bool // if set, the parser is parsing a rhs expression
// Ordinary identifier scopes
pkgScope *ast.Scope // pkgScope.Outer == nil
topScope *ast.Scope // top-most scope; may be pkgScope
unresolved []*ast.Ident // unresolved identifiers
imports []*ast.ImportSpec // list of imports
// Label scopes
// (maintained by open/close LabelScope)
labelScope *ast.Scope // label scope for current function
targetStack [][]*ast.Ident // stack of unresolved labels } 解析的入口是ParseFile,首先调用init,再调用parseFile进行解析. 整个解析是一个递归向下的过程也就是最low但是最实用的手写实现的方式.像yacc[4]生成的是我们编译里学的LALR[5]文法,牛逼的一逼,但是 gcc和Go都没用自动生成的解析器,也就是手写个几千行代码的事,所以为了更好的掌握编译器的细节,都选择了手写最简单的递归向下的方式.
通过init初始化scanner等.
func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
p.file = fset.AddFile(filename, -1, len(src))
var m scanner.Mode
if mode&ParseComments != 0 {
m = scanner.ScanComments
}
// 错误处理函数是在错误列表中添加错误.
eh := func(pos token.Position, msg string) { p.errors.Add(pos, msg) }
p.scanner.Init(p.file, src, eh, m)
p.mode = mode
p.trace = mode&Trace != 0 // for convenience (p.trace is used frequently)
p.next() } parseFile的简化流程:
// package clause
// 获取源文件开头的doc注释,从这里递归向下的解析开始了
doc := p.leadComment
// expect 从scanner获取一个token,并且返回位置pos.
pos := p.expect(token.PACKAGE)
// parseIdent 获取一个token并且转化为indent,如果不是报错.
ident := p.parseIdent()
if ident.Name == "_" && p.mode&DeclarationErrors != 0 {
p.error(p.pos, "invalid package name _")
}
// 作用域开始,标记解释器当前开始一个新的作用域
p.openScope()
// pkgScope 就是现在进入的作用域
p.pkgScope = p.topScope
// 解析 import 申明
for p.tok == token.IMPORT {
// parseGenDecl解析的是
// import (
// )
// 这样的结构,如果有括号就用parseImportSpec解析列表
// 没有就单独解析.
// 而parseImportSpec解析的是 一个可选的indent token和一个字符串token.
// 并且加入到imports列表中.
decls = append(decls, p.parseGenDecl(token.IMPORT, p.parseImportSpec))
}
// 解析全局的申明,包括函数申明
if p.mode&ImportsOnly == 0 {
// rest of package body
for p.tok != token.EOF {
decls = append(decls, p.parseDecl(syncDecl))
}
}
// 标记从当前作用域离开.
p.closeScope()
// 最后返回ast.File文件对象.
return &ast.File{
Doc: doc,
Package: pos,
Name: ident,
Decls: decls,
Scope: p.pkgScope,
Imports: p.imports,
Unresolved: p.unresolved[0:i],
Comments: p.comments,
} 看一下parseDecl主要是根据类型的不同调用不同的解析函数,parseValueSpec解析Value类型,parseTypeSpec解析Type类型,parseFuncDecl解析函数. 解析定义和解析类型的都是解析了,类似于var|type ( ident valueSpec|typeSpec)的token结构.因为parseFuncDecl里面也会解析这些内容,所以直接从函数解析来看也可以. 因为外一层的top scope其实就是相当于一个抽象的函数作用域而已,这样是为什么len和new这样的内嵌函数在函数内是可以做变量名的原因,因为可以在子作用域覆盖top作用域.整个解析过程简化过程如下.
// 解析一个func.
pos := p.expect(token.FUNC)
// 开一个新的作用域,topScope作为父Scope.
scope := ast.NewScope(p.topScope) // function scope
// 解析一个ident作为函数名
ident := p.parseIdent()
// 解析函数签名,也就是参数和返回值
params, results := p.parseSignature(scope)
// 再解析body
body = p.parseBody(scope)
// 最后返回函数申明.
decl := &ast.FuncDecl{
Doc: doc,
Recv: recv,
Name: ident,
Type: &ast.FuncType{
Func: pos,
Params: params,
Results: results,
},
Body: body,
} 解析参数和返回值就是解析(filed,filed)这样的格式,每个filed是indent type的token,最后构造成函数签名.然后来到parseBody,这个函数其实就是解析了左右花括号,然后向下开始解析Statement列表,类似于body -> { stmt_list },然后进入stmt_list的解析,不断地解析statement.
for p.tok != token.CASE && p.tok != token.DEFAULT && p.tok != token.RBRACE && p.tok != token.EOF {
list = append(list, p.parseStmt())
} parseStmt最后会进入到语句的解析,然后根据不同的token选择进入不同的解析流程,比如看到var,type,const就是申明,碰到标识符和数字等等可能就是单独的表达式, 如果碰到go,就知道是一个go语句,如果看到defer和return都能判断出相应的语句并按规则解析,看到break等条件关键字就解析条件语句,看到{就解析块语句.都是可以递归去解析的.
func (p *parser) parseStmt() (s ast.Stmt) {
if p.trace {
defer un(trace(p, “Statement”))
}
switch p.tok {
case token.CONST, token.TYPE, token.VAR:
s = &ast.DeclStmt{Decl: p.parseDecl(syncStmt)}
case
// tokens that may start an expression
token.IDENT, token.INT, token.FLOAT, token.IMAG, token.CHAR, token.STRING, token.FUNC, token.LPAREN, // operands
token.LBRACK, token.STRUCT, token.MAP, token.CHAN, token.INTERFACE, // composite types
token.ADD, token.SUB, token.MUL, token.AND, token.XOR, token.ARROW, token.NOT: // unary operators
s, _ = p.parseSimpleStmt(labelOk)
// because of the required look-ahead, labeled statements are
// parsed by parseSimpleStmt - don't expect a semicolon after
// them
if _, isLabeledStmt := s.(*ast.LabeledStmt); !isLabeledStmt {
p.expectSemi()
}
case token.GO:
s = p.parseGoStmt()
case token.DEFER:
s = p.parseDeferStmt()
case token.RETURN:
s = p.parseReturnStmt()
case token.BREAK, token.CONTINUE, token.GOTO, token.FALLTHROUGH:
s = p.parseBranchStmt(p.tok)
case token.LBRACE:
s = p.parseBlockStmt()
...省略 举个例子看一下parseSimpleStmt()的简化流程
// 解析左列表 一般是 l := r 或者 l1,l2 = r1,r2 或者 l <- r 或者 l++
x := p.parseLhsList()
switch p.tok {
case
token.DEFINE, token.ASSIGN, token.ADD_ASSIGN,
token.SUB_ASSIGN, token.MUL_ASSIGN, token.QUO_ASSIGN,
token.REM_ASSIGN, token.AND_ASSIGN, token.OR_ASSIGN,
token.XOR_ASSIGN, token.SHL_ASSIGN, token.SHR_ASSIGN, token.AND_NOT_ASSIGN:
// 如果看到range,range作为一种运算符按照range rhs来解析
// 如果没看到就按正常赋值语句解析 lhs op rhs 来解析op可以是上面那些token中的一种.
pos, tok := p.pos, p.tok
p.next()
var y []ast.Expr
isRange := false
if mode == rangeOk && p.tok == token.RANGE && (tok == token.DEFINE || tok == token.ASSIGN) {
pos := p.pos
p.next()
y = []ast.Expr{&ast.UnaryExpr{OpPos: pos, Op: token.RANGE, X: p.parseRhs()}}
isRange = true
} else {
y = p.parseRhsList()
}
as := &ast.AssignStmt{Lhs: x, TokPos: pos, Tok: tok, Rhs: y}
// 碰到":"找一个ident, 构成 goto: indent 之类的语句.
case token.COLON:
colon := p.pos
p.next()
if label, isIdent := x[0].(*ast.Ident); mode == labelOk && isIdent {
// Go spec: The scope of a label is the body of the function
// in which it is declared and excludes the body of any nested
// function.
stmt := &ast.LabeledStmt{Label: label, Colon: colon, Stmt: p.parseStmt()}
p.declare(stmt, nil, p.labelScope, ast.Lbl, label)
return stmt, false
}
// 碰到"<-",就构成 <- rhs 这样的语句.
case token.ARROW:
// send statement
arrow := p.pos
p.next()
y := p.parseRhs()
return &ast.SendStmt{Chan: x[0], Arrow: arrow, Value: y}, false
// 碰到"++"或者"--"就构成一个单独的自增语句.
case token.INC, token.DEC:
// increment or decrement
s := &ast.IncDecStmt{X: x[0], TokPos: p.pos, Tok: p.tok}
p.next()
return s, false
} 接下来就不一一解释每段代码了,具体情况具体看就可以.这里举个例子.
package main
import (
“go/ast”
“go/parser”
“go/token”
)
func main() {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, “”, `
package main
func main(){
// comments
x:=1
go println(x)
}
`, parser.ParseComments)
if err != nil {
panic(err)
}
ast.Print(fset, f)
}
产生的结果是
0 *ast.File {
1 . Package: 2:1 |PACKAGE token
2 . Name: *ast.Ident { |IDENT token
3 . . NamePos: 2:9 |
4 . . Name: "main" |
5 . } |整个构成了顶部的 package main
6 . Decls: []ast.Decl (len = 1) { |最上层的申明列表
7 . . 0: *ast.FuncDecl { |func main的函数申明
8 . . . Name: *ast.Ident { |IDENT token
9 . . . . NamePos: 3:6 |
10 . . . . Name: "main" |
11 . . . . Obj: *ast.Object { |Objec是一个用于表达语法对象的结构
12 . . . . . Kind: func |表示之前存在过,Decl指向了7,也就是第7行的FuncDecl.
13 . . . . . Name: "main" |
14 . . . . . Decl: *(obj @ 7) |
15 . . . . } |
16 . . . } |
17 . . . Type: *ast.FuncType { |函数类型,也就是函数签名
18 . . . . Func: 3:1 |参数和返回值都是空的
19 . . . . Params: *ast.FieldList { |
20 . . . . . Opening: 3:10
21 . . . . . Closing: 3:11
22 . . . . }
23 . . . }
24 . . . Body: *ast.BlockStmt { |块语句,也就是main的body
25 . . . . Lbrace: 3:12
26 . . . . List: []ast.Stmt (len = 2) { |语句列表
27 . . . . . 0: *ast.AssignStmt { |赋值语句
28 . . . . . . Lhs: []ast.Expr (len = 1) { |左值是x
29 . . . . . . . 0: *ast.Ident {
30 . . . . . . . . NamePos: 5:2 |
31 . . . . . . . . Name: "x"
32 . . . . . . . . Obj: *ast.Object { |
33 . . . . . . . . . Kind: var
34 . . . . . . . . . Name: "x" |
35 . . . . . . . . . Decl: *(obj @ 27)
36 . . . . . . . . }
37 . . . . . . . } |
38 . . . . . . }
39 . . . . . . TokPos: 5:3 |:=和它的位置
40 . . . . . . Tok: :=
41 . . . . . . Rhs: []ast.Expr (len = 1) { |右边是一个数字类型的token
42 . . . . . . . 0: *ast.BasicLit {
43 . . . . . . . . ValuePos: 5:5
44 . . . . . . . . Kind: INT
45 . . . . . . . . Value: "1"
46 . . . . . . . }
47 . . . . . . }
48 . . . . . }
49 . . . . . 1: *ast.GoStmt { |接下来是go语句
50 . . . . . . Go: 6:2
51 . . . . . . Call: *ast.CallExpr { |一个调用表达式
52 . . . . . . . Fun: *ast.Ident { |IDENT token是println
53 . . . . . . . . NamePos: 6:5
54 . . . . . . . . Name: "println"
55 . . . . . . . }
56 . . . . . . . Lparen: 6:12 |左括号的位置
57 . . . . . . . Args: []ast.Expr (len = 1) { |参数列表
58 . . . . . . . . 0: *ast.Ident { |是一个符号INDENT,并且指向的是32行的x
59 . . . . . . . . . NamePos: 6:13
60 . . . . . . . . . Name: "x"
61 . . . . . . . . . Obj: *(obj @ 32)
62 . . . . . . . . }
63 . . . . . . . }
64 . . . . . . . Ellipsis: -
65 . . . . . . . Rparen: 6:14 |右括号的位置
66 . . . . . . }
67 . . . . . }
68 . . . . }
69 . . . . Rbrace: 8:1
70 . . . }
71 . . }
72 . }
73 . Scope: *ast.Scope { |最顶级的作用域
74 . . Objects: map[string]*ast.Object (len = 1) {
75 . . . "main": *(obj @ 11)
76 . . }
77 . }
78 . Unresolved: []*ast.Ident (len = 1) { |这里有个没有定义的符号println,是因为是内置符号,会另外处理
79 . . 0: *(obj @ 52) |从源文件上是表现不出来的.
80 . }
81 . Comments: []*ast.CommentGroup (len = 1) { |评论列表,以及位置和内容.
82 . . 0: *ast.CommentGroup {
83 . . . List: []*ast.Comment (len = 1) {
84 . . . . 0: *ast.Comment {
85 . . . . . Slash: 4:2
86 . . . . . Text: "// comments"
87 . . . . }
88 . . . }
89 . . }
90 . }
91 } 这就是Go的整个语法分析和最后产生的语法树的结构.
废话说了这么多其实实现很简单,问题是如何把一个语言的spec定义好,很重要,早期语言设计不是很固定的.都是慢慢尝试不断改进的过程.最早的一次spec文档[6]其实和现在差了很多很多.就是把TOKEN记号流从左至右匹配规则(可能会向前看几个token),然后递归解析语法树,最后得到AST.
我在我的字符画转换器里用的也是类似的方式[7],做了自顶向下递归解析语法的方式,但是错误处理都是速错,不会做错误恢复找到一个可以同步的节点继续分析.
所以这里补充一点,Go是如何进行错误处理的同步问题,寄希望于能够向使用者提供更多的错误.主要是parser当中的两个结构
syncPos token.Pos // last synchronization position
syncCnt int // number of calls to syncXXX without progress syncPos错误的同步位置,也就类似于游戏的存档点,如果发生错误那就从这个地方开始跳过(BadStmt|BadExpr)继续解析,在每次完成语句,申明或者表达式的解析之后就会保存一个同步点.虽然这种继续解析的行为不一定能够给出很精确的错误提示,但的确够用了.当然如果错误实在太多了,从同步点恢复也没有太大意义,就会主动放弃,所以记录了没有成功解析而同步的次数.
因为之前造过轮子了,所以我发现其实编译器的前端用手写是一个很繁琐并且需要花很多时间去做的一件事情,如果语言有设计良好,那么也至少需要花实现的时间,如果设计不好,实现也要跟着修修补补,那就更麻烦,虽然整个编译器的前端也就不到万行代码,但是的确是很考验耐心的一件事情,而且用递归向下的方式解析也没什么效率问题,编译器编译慢一点也不是很要紧,所以有轮子还是用轮子吧,这只是一件苦力活,的确没什么高科技.
最后附带一个用Go实现的Go语法的子集的动态语言版本,只有几十行.
https://gist.github.com/ggaaooppeenngg/dff0fff8f0c9194d93c70550d50edbfa
https://gist.github.com/ggaaooppeenngg/dff0fff8f0c9194d93c70550d50edbfa
https://huang-jerryc.com/2016/03/15/%E4%BD%95%E4%B8%BA%E8%AF%AD%E6%B3%95%E6%A0%91/
再窥视一下JavaScript的语法树
在语法复杂的语言中,语法树是包含很多细节的语法结果表达式,我们需要靠语法树把这种形式以更简洁的形式表达出来。
Javascript 有不少工具可以把代码构造出清晰的语法树,比如 esprima、v8、SpiderMonkey、UglifyJS、AST explorer等。
https://www.php.cn/manual/view/35199.html
https://blog.csdn.net/weixin_33896726/article/details/93181205
https://www.jianshu.com/p/937d649039ec
https://www.shangmayuan.com/a/90f086113eef412aa353e0be.html
许多自动化代码生成工具都离不开语法树分析,例如goimport,gomock,wire等项目都离不开语法树分析。基于语法树分析,能够实现许多有趣实用的工具。本篇将结合示例,展现如何基于ast标准包操做语法树。node
本篇中的代码的完整示例能够在这里找到:ast-examplegit
Quick Start
首先咱们看下语法树长什么样子,如下代码将打印./demo.go文件的语法树:github
package main
import (
“go/ast”
“go/parser”
“go/token”
“log”
“path/filepath”
)
func main() {
fset := token.NewFileSet()
// 这里取绝对路径,方便打印出来的语法树能够转跳到编辑器
path, _ := filepath.Abs(“./demo.go”)
f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Println(err)
return
}
// 打印语法树
ast.Print(fset, f)
}
复制代码
demo.go:golang
package main
import (
“context”
)
// Foo 结构体
type Foo struct {
i int
}
// Bar 接口
type Bar interface {
Do(ctx context.Context) error
}
// main方法
func main() {
a := 1
}
复制代码
demo.go文件已尽可能简化,但其语法树的输出内容依旧十分庞大。咱们截取部分来作一些简要的说明。express
首先是文件所属的包名,和其声明在文件中的位置:bash
0 *ast.File {
1 . Package: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:9
4 . . Name: “main”
5 . }
…
复制代码
紧接着是Decls,也就是Declarations,其包含了声明的一些变量,方法,接口等:app
…
6 . Decls: []ast.Decl (len = 4) {
7 . . 0: ast.GenDecl {
8 . . . TokPos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:3:1
9 . . . Tok: import
10 . . . Lparen: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:3:8
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:4:2
15 . . . . . . Kind: STRING
16 . . . . . . Value: “"context"”
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:5:1
22 . . }
….
复制代码
能够看到该语法树包含了4条Decl记录,咱们取第一条记录为例,该记录为ast.GenDecl类型。不难看出这条记录对应的是咱们的import代码段。始位置(TokPos),左右括号的位置(Lparen,Rparen),和import的包(Specs)等信息都能从语法树中获得。编辑器
语法树的打印信来自ast.File结构体:ide
$GOROOT/src/go/ast/ast.go函数
// 该结构体位于标准包 go/ast/ast.go 中,有兴趣能够转跳到源码阅读更详尽的注释
type File struct {
Doc CommentGroup // associated documentation; or nil
Package token.Pos // position of “package” keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []ImportSpec // imports in this file
Unresolved []Ident // unresolved identifiers in this file
Comments []CommentGroup // list of all comments in the source file
}
复制代码
结合注释和字段名咱们大概知道每一个字段的含义,接下来咱们详细梳理一下语法树的组成结构。
Node节点
整个语法树由不一样的node组成,从源码注释中能够得知主要有以下三种node:
There are 3 main classes of nodes: Expressions and type nodes, statement nodes, and declaration nodes.
在Go的Language Specification中能够找到这些节点类型详细规范和说明,有兴趣的小伙伴能够深刻研究一下,在此不作展开。
但实际在代码,出现了第四种node:Spec Node,每种node都有专门的接口定义:
$GOROOT/src/go/ast/ast.go
…
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
…
// A Spec node represents a single (non-parenthesized) import,
// constant, type, or variable declaration.
//
type (
// The Spec type stands for any of *ImportSpec, *ValueSpec, and *TypeSpec.
Spec interface {
Node
specNode()
}
….
)
复制代码
能够看到全部的node都继承Node接口,记录了node的开始和结束位置。还记得Quick Start示例中的Decls吗?它正是declaration nodes。除去上述四种使用接口进行分类的node,还有些node没有再额外定义接口细分类别,仅实现了Node接口,为了方便描述,在本篇中我把这些节点称为common node。 $GOROOT/src/go/ast/ast.go列举了全部全部节点的实现,咱们从中挑选几个做为例子,感觉一下它们的区别。
Expression and Type
先来看expression node。
$GOROOT/src/go/ast/ast.go
…
// An Ident node represents an identifier.
Ident struct {
NamePos token.Pos // identifier position
Name string // identifier name
Obj *Object // denoted object; or nil
}
…
复制代码
Indent(identifier)表示一个标识符,好比Quick Start示例中表示包名的Name字段就是一个expression node:
0 *ast.File {
1 . Package: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:1
2 . Name: *ast.Ident { <—-
3 . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:1:9
4 . . Name: “main”
5 . }
…
复制代码
接下来是type node。
$GOROOT/src/go/ast/ast.go
…
// A StructType node represents a struct type.
StructType struct {
Struct token.Pos // position of “struct” keyword
Fields *FieldList // list of field declarations
Incomplete bool // true if (source) fields are missing in the Fields list
}
// Pointer types are represented via StarExpr nodes.
// A FuncType node represents a function type.
FuncType struct {
Func token.Pos // position of "func" keyword (token.NoPos if there is no "func")
Params *FieldList // (incoming) parameters; non-nil
Results *FieldList // (outgoing) results; or nil
}
// An InterfaceType node represents an interface type.
InterfaceType struct {
Interface token.Pos // position of "interface" keyword
Methods *FieldList // list of methods
Incomplete bool // true if (source) methods are missing in the Methods list
} ... 复制代码 type node很好理解,它包含一些复合类型,例如在Quick Start中出现的StructType,FuncType和InterfaceType。
Statement
赋值语句,控制语句(if,else,for,select…)等均属于statement node。
$GOROOT/src/go/ast/ast.go
…
// An AssignStmt node represents an assignment or
// a short variable declaration.
//
AssignStmt struct {
Lhs []Expr
TokPos token.Pos // position of Tok
Tok token.Token // assignment token, DEFINE
Rhs []Expr
}
…
// An IfStmt node represents an if statement.
IfStmt struct {
If token.Pos // position of "if" keyword
Init Stmt // initialization statement; or nil
Cond Expr // condition
Body *BlockStmt
Else Stmt // else branch; or nil
} ... 复制代码 例如Quick Start中,咱们在main函数中对变量a赋值的程序片断就属于AssignStmt:
…
174 . . . Body: *ast.BlockStmt {
175 . . . . Lbrace: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:18:13
176 . . . . List: []ast.Stmt (len = 1) {
177 . . . . . 0: *ast.AssignStmt { <— 这里
178 . . . . . . Lhs: []ast.Expr (len = 1) {
179 . . . . . . . 0: *ast.Ident {
180 . . . . . . . . NamePos: /usr/local/gopath/src/github.com/DrmagicE/ast-example/quickstart/demo.go:19:2
181 . . . . . . . . Name: “a”
…
复制代码
Spec Node
Spec node只有3种,分别是ImportSpec,ValueSpec和TypeSpec:
$GOROOT/src/go/ast/ast.go
// An ImportSpec node represents a single package import.
ImportSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // local package name (including "."); or nil
Path *BasicLit // import path
Comment *CommentGroup // line comments; or nil
EndPos token.Pos // end of spec (overrides Path.Pos if nonzero)
}
// A ValueSpec node represents a constant or variable declaration
// (ConstSpec or VarSpec production).
//
ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}
// A TypeSpec node represents a type declaration (TypeSpec production).
TypeSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // type name
Assign token.Pos // position of '=', if any
Type Expr // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
Comment *CommentGroup // line comments; or nil
} 复制代码 ImportSpec表示一个单独的import,ValueSpec表示一个常量或变量的声明,TypeSpec则表示一个type声明。例如 在Quick Start示例中,出现了ImportSpec和TypeSpec
import (
“context” // <— 这里是一个ImportSpec node
)
// Foo 结构体
type Foo struct { // <— 这里是一个TypeSpec node
i int
}
复制代码
在语法树的打印结果中能够看到对应的输出,小伙伴们可自行查找。
Declaration Node
Declaration node也只有三种:
$GOROOT/src/go/ast/ast.go
…
type (
// A BadDecl node is a placeholder for declarations containing
// syntax errors for which no correct declaration nodes can be
// created.
//
BadDecl struct {
From, To token.Pos // position range of bad declaration
}
// A GenDecl node (generic declaration node) represents an import,
// constant, type or variable declaration. A valid Lparen position
// (Lparen.IsValid()) indicates a parenthesized declaration.
//
// Relationship between Tok value and Specs element type:
//
// token.IMPORT *ImportSpec
// token.CONST *ValueSpec
// token.TYPE *TypeSpec
// token.VAR *ValueSpec
//
GenDecl struct {
Doc *CommentGroup // associated documentation; or nil
TokPos token.Pos // position of Tok
Tok token.Token // IMPORT, CONST, TYPE, VAR
Lparen token.Pos // position of '(', if any
Specs []Spec
Rparen token.Pos // position of ')', if any
}
// A FuncDecl node represents a function declaration.
FuncDecl struct {
Doc *CommentGroup // associated documentation; or nil
Recv *FieldList // receiver (methods); or nil (functions)
Name *Ident // function/method name
Type *FuncType // function signature: parameters, results, and position of "func" keyword
Body *BlockStmt // function body; or nil for external (non-Go) function
} ) ... 复制代码 BadDecl表示一个有语法错误的节点; GenDecl用于表示import, const,type或变量声明;FunDecl用于表示函数声明。 GenDecl和FunDecl在Quick Start例子中均有出现,小伙伴们可自行查找。
Common Node
除去上述四种类别划分的node,还有一些node不属于上面四种类别:
$GOROOT/src/go/ast/ast.go
// Comment 注释节点,表明单行的 //-格式 或 /*-格式的注释.
type Comment struct {
…
}
…
// CommentGroup 注释块节点,包含多个连续的Comment
type CommentGroup struct {
…
}
// Field 字段节点, 能够表明结构体定义中的字段,接口定义中的方法列表,函数前面中的入参和返回值字段
type Field struct {
…
}
…
// FieldList 包含多个Field
type FieldList struct {
…
}
// File 表示一个文件节点
type File struct {
…
}
// Package 表示一个包节点
type Package struct {
…
}
复制代码
Quick Start示例包含了上面列举的全部node,小伙伴们能够自行查找。更为详细的注释和具体的结构体字段请查阅源码。
全部的节点类型大体列举完毕,其中还有许多具体的节点类型未能一一列举,但基本上都是大同小异,源码注释也比较清晰,等用到的时候再细看也不迟。如今咱们对整个语法树的构造有了基本的了解,接下来经过几个示例来演示具体用法。
示例
为文件中全部接口方法添加context参数
实现这个功能咱们须要四步:
遍历整个语法树
判断是否已经importcontext包,若是没有则import
遍历全部的接口方法,判断方法列表中是否有context.Context类型的入参,若是没有咱们将其添加到方法的第一个参数
将修改事后的语法树转换成Go代码并输出
遍历语法树
语法树层级较深,嵌套关系复杂,若是不能彻底掌握node之间的关系和嵌套规则,咱们很难本身写出正确的遍历方法。不过好在ast包已经为咱们提供了遍历方法:
$GOROOT/src/go/ast/ast.go
func Walk(v Visitor, node Node)
复制代码
type Visitor interface {
Visit(node Node) (w Visitor)
}
复制代码
Walk方法会按照深度优先搜索方法(depth-first order)遍历整个语法树,咱们只需按照咱们的业务须要,实现Visitor接口便可。 Walk每遍历一个节点就会调用Visitor.Visit方法,传入当前节点。若是Visit返回nil,则中止遍历当前节点的子节点。本示例的Visitor实现以下:
// Visitor
type Visitor struct {
}
func (v Visitor) Visit(node ast.Node) ast.Visitor {
switch node.(type) {
case *ast.GenDecl:
genDecl := node.(ast.GenDecl)
// 查找有没有import context包
// Notice:没有考虑没有import任何包的状况
if genDecl.Tok == token.IMPORT {
v.addImport(genDecl)
// 不须要再遍历子树
return nil
}
case ast.InterfaceType:
// 遍历全部的接口类型
iface := node.(ast.InterfaceType)
addContext(iface)
// 不须要再遍历子树
return nil
}
return v
}
复制代码
添加import
// addImport 引入context包
func (v Visitor) addImport(genDecl *ast.GenDecl) {
// 是否已经import
hasImported := false
for _, v := range genDecl.Specs {
imptSpec := v.(ast.ImportSpec)
// 若是已经包含”context”
if imptSpec.Path.Value == strconv.Quote(“context”) {
hasImported = true
}
}
// 若是没有import context,则import
if !hasImported {
genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote(“context”),
},
})
}
}
复制代码
为接口方法添加参数
// addContext 添加context参数
func addContext(iface ast.InterfaceType) {
// 接口方法不为空时,遍历接口方法
if iface.Methods != nil || iface.Methods.List != nil {
for _, v := range iface.Methods.List {
ft := v.Type.(ast.FuncType)
hasContext := false
// 判断参数中是否包含context.Context类型
for _, v := range ft.Params.List {
if expr, ok := v.Type.(ast.SelectorExpr); ok {
if ident, ok := expr.X.(ast.Ident); ok {
if ident.Name == “context” {
hasContext = true
}
}
}
}
// 为没有context参数的方法添加context参数
if !hasContext {
ctxField := &ast.Field{
Names: []ast.Ident{
ast.NewIdent(“ctx”),
},
// Notice: 没有考虑import别名的状况
Type: &ast.SelectorExpr{
X: ast.NewIdent(“context”),
Sel: ast.NewIdent(“Context”),
},
}
list := []ast.Field{
ctxField,
}
ft.Params.List = append(list, ft.Params.List…)
}
}
}
}
复制代码
将语法树转换成Go代码
format包为咱们提供了转换函数,format.Node会将语法树按照gofmt的格式输出:
…
var output []byte
buffer := bytes.NewBuffer(output)
err = format.Node(buffer, fset, f)
if err != nil {
log.Fatal(err)
}
// 输出Go代码
fmt.Println(buffer.String())
…
复制代码
输出结果以下:
package main
import (
“context”
)
type Foo interface {
FooA(ctx context.Context, i int)
FooB(ctx context.Context, j int)
FooC(ctx context.Context)
}
type Bar interface {
BarA(ctx context.Context, i int)
BarB(ctx context.Context)
BarC(ctx context.Context)
}
复制代码
能够看到咱们全部的接口方的第一个参数都变成了context.Context。建议将示例中的语法树先打印出来,再对照着代码看,方便理解。
一些坑与不足
至此咱们已经完成了语法树的解析,遍历,修改以及输出。但细心的小伙伴可能已经发现:示例中的文件并无出现一行注释。这的确是有意为之,若是咱们加上注释,会发现最终生成文件的注释就像迷途的羔羊,彻底找不到本身的位置。好比这样:
//修改前
type Foo interface {
FooA(i int)
// FooB
FooB(j int)
FooC(ctx context.Context)
}
// 修改后
type Foo interface {
FooA(ctx context.
// FooB
Context, i int)
FooB(ctx context.Context, j int)
FooC(ctx context.Context) } 复制代码 致使这种现象的缘由在于:ast包生成的语法树中的注释是"free-floating"的。还记得每一个node都有Pos()和End()方法来标识其位置吗?对于非注释节点,语法树可以正确的调整他们的位置,但却不能自动调整注释节点的位置。若是咱们想要让注释出如今正确的位置上,咱们必须手动设置节点Pos和End。源码注释中提到了这个问题:
Whether and how a comment is associated with a node depends on the interpretation of the syntax tree by the manipulating program: Except for Doc and Comment comments directly associated with nodes, the remaining comments are “free-floating” (see also issues #18593, #20744).
issue中有具体的讨论,官方认可这是一个设计缺陷,但仍是迟迟未能改进。其中有位火烧眉毛的小哥提供了本身的方案:
github.com/dave/dst
若是实在是要对有注释的语法树进行修改,能够尝试一下。 虽然语法树的确存在修改困难问题,但其仍是能知足大部分基于语法树分析的代码生成工做了(gomock,wire等等)。
https://studygolang.com/articles/19353?fr=sidebar
https://blog.csdn.net/weixin_33851429/article/details/91449588
https://www.shangmayuan.com/a/42992bc766534cba9658bca9.html
https://www.jianshu.com/p/428d663cb2d8
Go语言有很多工具, goimports用于package的自动导入或者删除, golint用于检查源码中不符合Go coding style的地方, 比如全名,注释等. 还有其它工具如gorename, guru等工具. 作为工具它们都是使用go语言(查看)开发的, 这些工具都有一个共同点就是: 读取源代码, 分析源代码, 修改或生成新代码.
简述
很多编程语言/库/框架等都能生成代码, 比如使用rails, 可以轻松地new一个project出来, 生成项目基本代码, 我们称其为boilerplate, 或者template, 这已经习以为常了. 像ruby的动态语言通常能在运行时生成代码, 我们称之为meta programming(元编程), 比如rails的resources可以生成restful的router出来.因为是运行时动态生成, 因此可能会遇到exception, 以及性能方面有所损失.
像elixir这种编程语言的macro则比ruby的元编程方面向”前”一步, 它在编译期生成代码, 而不在运行时生成, 好处是可以生成大量的代码而对性能几乎没有太大影响. 像phoenix框架的router查看部分, 则通过macro生成大量的函数, 利用BEAM的pattern matching机制高效路由.elixir的macro是写在源代码里的, 而Go则可以分离.
Go语言可以通过reflect包同样做到ruby的运行时生成代码(比如创建对象), 但更强大的一点是, 它通过读取源码, 再修改源码, 生成新的代码.我们可以将这个过程单独写作一个工具, 这个工具可以适用于不同的项目.
例子
stringer
package game
//go:generate stringer -type=GameStatus
// 注意//与go:generate字符之间不能有空格
// GameStatus 表示比赛的状态
type GameStatus int
const (
Unvalid GameStatus = iota
ValidFailed
Valid
Register
Start
Running
End
)
运行 go generate 会生成gamestatus_string.go文件, 并且实现了Stringer接口.
同样的例子在gRPC中也出现过code, 生成的string.正如Rob Pike所说:
let the mechine do the work.source
gen_columns
很多项目在使用数据库时, 通过tag指定数据库里的字段名字, 在写SQL时, 又只能通过字符串来表示字段名, 因此如果某一个字段名修改时, 则意味着涉及到此字段的SQL都面临着修改, 而我们希望只需要修改一个地方.
有一个结构作为数据库表结构如下:
1
2
3
4
type User struct {
ID int json:"id" bson:"id"
Name string json:"name" bson:"name"
}
当使用这个model里的字段进行sql查询时, 通常使用:
1
2
3
map[string]interface{}{
“id”:123456,
}
作为查询条件, 如果当字段名更改时, 不得不修改这个map里的key值
如果能够自动生成一个结构体, 用于表示这些column name值, 那么只需修改一处:
1
2
3
map[string]interface{}{
UserColumns.ID: 123456
}
使用方法
1
gen_columns -tag=”bson” -path=”./models/user.go”
会生成一个独立的文件, 里面的内容为:
package models
type _UserColumn struct {
ID string
Name string
}
var UserColumns _UserColumn
func init() {
UserColumns.ID = “id”
UserColumns.Name = “name”
}
总结
gen_columns是自己在项目中遇到问题所给出的解决办法, 第一版本是通过reflect做的, 总共需要好几个步骤; 使用ast做就只需在编译时多加一个go generate, 而这命令基本上可以集成在build的脚本里, 因此不需要再额外担心代码生成的问题.
让我们用Go创造更多生成代码的工具吧.
https://www.cnblogs.com/qgymje/p/5879375.html
https://studygolang.com/articles/5147
Tokenizer 和 Lexical anaylizer
如果你知道tokenizer和lexical anaylizer是什么的话,请跳到下一章,不熟的话请看下面这个最简单的go代码
package main
func main() {
println(“Hello, World!”)
}
这段go代码做了什么?很简单吧,package是main,定义了个main函数,main函数里调用了println函数,参数是”Hello, World!“。好,你是知道了,可当你运行go run时,go怎么知道的?go先要把你的代码打散成自己可以理解的构成部分(token),这一过程就叫tokenize。例如,第一行就被拆成了package和main。 这个阶段,go就像小婴儿只会理解我、要、吃饭等词,但串不成合适句子。因为”吃饭我要”是讲不通的,所以把词按一定的语法串起来的过程就是lexical anaylize或者parse,简单吧!和人脑不同的是,被程序理解的代码,通常会以abstract syntax tree(AST)的形式存储起来,方便进行校验和查找。
Go的AST
那我们来看看go的ast库对代码的理解程度是不是小婴儿吧(可运行的源代码在此),其实就是token+parse刚才我们看到的上一章代码,并且按AST的方式打印出来,结果在这里
package main
import (
“go/ast”
“go/parser”
“go/token”
)
func main() {
// 这就是上一章的代码.
src := `
package main
func main() {
println(“Hello, World!”)
}
`
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f)
}
为了不吓到你,我先只打印前6行:
0 *ast.File {
1 . Package: 2:1
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "main"
5 . }
// 省略之后的50+行 可见,go 解析出了package这个关键词在文本的第二行的第一个(2:1)。”main”也解析出来了,在第二行的第9个字符,但是go的解析器还给它安了一个叫法:ast.Ident, 标示符 或者大家常说的ID,如下图所示:
Ident +————+
|
Package +—–+ |
v v
package main
接下来我们看看那个main函数被整成了什么样。
6 . Decls: []ast.Decl (len = 1) {
7 . . 0: *ast.FuncDecl {
8 . . . Name: *ast.Ident {
9 . . . . NamePos: 3:6
10 . . . . Name: "main"
11 . . . . Obj: *ast.Object {
12 . . . . . Kind: func
13 . . . . . Name: "main"
14 . . . . . Decl: *(obj @ 7) 此处func main被解析成ast.FuncDecl(function declaration),而函数的参数(Params)和函数体(Body)自然也在这个FuncDecl中。Params对应的是*ast.FieldList,顾名思义就是项列表;而由大括号”{}”组成的函数体对应的是ast.BlockStmt(block statement)。如果不清楚,可以参考下面的图:
FuncDecl.Params +----------+
|
FuncDecl.Name +--------+ |
v v
+----------------------> func main() {
| +-> FuncDecl ++ FuncDecl.Body +-+ println("Hello, World!")
| +->
+----------------------> } 而对于main函数的函数体中,我们可以看到调用了println函数,在ast中对应的是ExprStmt(Express Statement),调用函数的表达式对应的是CallExpr(Call Expression),调用的参数自然不能错过,因为参数只有字符串,所以go把它归为ast.BasicLis (a literal of basic type)。如下图所示:
+—–+ ExprStmt +—————+
| |
| CallExpr BasicLit |
| + + |
| v v |
+—> println(“Hello, World!”)<–+
还有什么?
50 . Scope: ast.Scope {
51 . . Objects: map[string]ast.Object (len = 1) {
52 . . . “main”: (obj @ 11)
53 . . }
54 . }
55 . Unresolved: []ast.Ident (len = 1) {
56 . . 0: *(obj @ 29)
57 . }
58 }
我们可以看出ast还解析出了函数的作用域,以及作用域对应的对象。
https://www.cnblogs.com/skzxc/p/12944921.html
https://www.cntofu.com/book/73/ch7-ast/readme.md
https://zhuanlan.zhihu.com/p/137196665
http://www.verydoc.net/go/00003872.html
https://developer.51cto.com/art/201911/606075.htm
http://blog.sina.com.cn/s/blog_a2e9bb2b0102x0gm.html