文章目录
solution of golang数据结构分析
问题来源
工作中我们经常需要对数据结构进行各种维度的分析,例如,层次结构、静态规格、运行时内存大小、拥有的函数、实现的接口等等。
作者前段时间在进行数据库的定长化改造,其中涉及到需要对go数据结构的静态规格分析,下面将介绍两种可行的方案。
go的背景介绍
在进行分析前,先简单介绍下golang的特点,这对“用go的办法解决go的问题”有一定的引导作用,同时也是一种约束。
- go是一种需要编译才能运行的编程语言。
- go有比较严格的类型检查,拥有interface机制,拥有较为强大的反射机制,但缺少泛型机制。
- go的设计思路:简单即复杂,用简单的语法表达复杂的逻辑。
数据结构分析方案一——golang编译前端之AST
AST即抽象语法树。
阅读资料
golang提供的静态编译工具链:
Package loader loads a complete Go program from source code, parsing and type-checking the initial packages plus their transitive closure of dependencies. The ASTs and the derived facts are retained for later use.
Package ssa defines a representation of the elements of Go programs (packages, types, functions, variables and constants) using a static single-assignment (SSA) form intermediate representation (IR) for the bodies of functions.
Package pointer implements Andersen’s analysis, an inclusion-based pointer analysis algorithm first described in (Andersen, 1994).
AST是什么
一般的编译型语言的编译经历 词法分析、语法分析、语义分析、IR生成、代码优化、机器码生成 几个阶段。
go语言编译可大致分为词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成四个逻辑阶段。
词法分析的输入是源文件xxx.go
,输出是一组token
,包路径src\go\token
,一个token
可以理解为一个代码元素,分为几组。
- 特殊
token
。例如,ILLEGAL
(非法TOKEN)、EOF
(文件末尾)、COMMENT
(注释) - 字面
token
。例如,标识符IDENT
、数字INT
、字符串STRING
等等。 - 操作符
token
。+ - * / , . ; ( )
等等。 - 关键字
token
。var
,select
,chan
等。
注:词法分析阶段,会给源码的每行的最后添加上分号;
。这就是go代码每行最后不用加分号的原因。
语法分析的输入是词法分析的结果,输出是一颗树状结构的树。是由没有语法意义的一个一个单词,按照一定的文法,转化为有层次结构,有一定语义的语法树。每个 go 源代码文件最终都会被解析成一个独立的抽象语法树。树的根节点是一个*ast.File
的元素,下面不断的递归包含了文件内所有的语法元素,并且有了一定的层次关系。
0 package main
1 func main() {
2 println("Hello, World!")
3 }
如上代码,会被转化为如下的语法树,箭头部分是我加的注解。
// Output:
// 0 *ast.File {
// 1 . Package: 2:1
// 2 . Name: *ast.Ident {
// 3 . . NamePos: 2:9
// 4 . . Name: "main"
// 5 . }
// 6 . Decls: []ast.Decl (len = 1) { ——————————————>声明slice
// 7 . . 0: *ast.FuncDecl { ——————————————>函数定义元素
// 8 . . . Name: *ast.Ident { ——————————————>函数定义由 Name Type Body组成 其实还有Doc(关联的文档),Recv(Receiver)
// 9 . . . . NamePos: 3:6 ——————————————>函数定义的Name的位置
// 10 . . . . Name: "main"
// 11 . . . . Obj: *ast.Object { ——————————————>函数定义的Object
// 12 . . . . . Kind: func
// 13 . . . . . Name: "main"
// 14 . . . . . Decl: *(obj @ 7)
// 15 . . . . }
// 16 . . . }
// 17 . . . Type: *ast.FuncType { ——————————————>函数定义的type,包括入参,出参,和func关键字的位置。
// 18 . . . . Func: 3:1 ——————————————>func关键字的位置。
// 19 . . . . Params: *ast.FieldList { ——————————————>函数定义的入参。
// 20 . . . . . Opening: 3:10
// 21 . . . . . Closing: 3:11
// 22 . . . . }——————————————>函数没有出参,所以Results为nil。
// 23 . . . }
// 24 . . . Body: *ast.BlockStmt {——————————————>函数体。
// 25 . . . . Lbrace: 3:13——————————————>函数体左花括号。
// 26 . . . . List: []ast.Stmt (len = 1) {——————————————>函数体每一个statement,实现了ast.Stmt接口的都可以放进去。
// 27 . . . . . 0: *ast.ExprStmt {——————————————>第一个元素是一个表达式
// 28 . . . . . . X: *ast.CallExpr {——————————————>函数调用表达式
// 29 . . . . . . . Fun: *ast.Ident {——————————————>函数调用函数名
// 30 . . . . . . . . NamePos: 4:2
// 31 . . . . . . . . Name: "println"
// 32 . . . . . . . }
// 33 . . . . . . . Lparen: 4:9——————————————>函数调用左括号
// 34 . . . . . . . Args: []ast.Expr (len = 1) {——————————————>函数调用参数
// 35 . . . . . . . . 0: *ast.BasicLit {——————————————>函数调用第一个参数
// 36 . . . . . . . . . ValuePos: 4:10
// 37 . . . . . . . . . Kind: STRING
// 38 . . . . . . . . . Value: "\"Hello, World!\""
// 39 . . . . . . . . }
// 40 . . . . . . . }
// 41 . . . . . . . Ellipsis: -
// 42 . . . . . . . Rparen: 4:25——————————————>函数调用右
// 43 . . . . . . }
// 44 . . . . . }
// 45 . . . . }
// 46 . . . . Rbrace: 5:1——————————————>函数体右花括号。
// 47 . . . }
// 48 . . }
// 49 . }
// 50 . Scope: *ast.Scope {——————————————>作用域信息。
// 51 . . Objects: map[string]*ast.Object (len = 1) {
// 52 . . . "main": *(obj @ 11)
// 53 . . }
// 54 . }
// 55 . Unresolved: []*ast.Ident (len = 1) {——————————————>未识别的Ident,此处为29行的println。
// 56 . . 0: *(obj @ 29)
// 57 . }
// 58 }
方案的可行性分析
语法树是源码的另一种表现形式,他一定包含了源码的所有信息,根据树状的结构,我们也可以很方便的通过递归方式获取想要的元素。
*ast.StructType
是我们这次数据结构分析关心的语法元素,我们可以对StructType
进行深入的分析,从而找到我们想要的信息。
优劣势分析
相比于直接读取源码,进行模式匹配,语法树的方式更加方便和准确,不需要自己写正则表达式,遍历也更简单。语法树的元素类型提供了更强大的模式匹配能力。
相比于反射方式,语法树可以和待分析的结构解耦,不需要import
待分析结构所在的package
,对源码的依赖性弱,较为灵活。
劣势是需要预备一些语法树的知识,了解go词法、语法分析的工具链。分析速度比反射稍慢,不过分析一般是一次性的,性能不是主要考虑因素。另一个劣势是,它是静态的,runtime
的场景不适用。
还能用ast做什么
ast的功能比较强大,用上面提到的pointer、loader等强大的工具链,我们可以对函数调用关系、对象依赖关系进行更深入的分析。
可以用于代码生成,代码替换,代码写作模式分析(编程规范识别)。
数据结构分析方案二——golang反射特性
阅读资料
反射的简单解释
反射是一种程序能够检查其自身结构的能力,尤其是通过类型信息。这是元编程的一种形式。它建立在golang的类型系统上。
可行性分析
反射是基于类型系统的,而我们要进行的正是类型分析。我们可以通过reflect.Type
的Kind
枚举得到我们关心的信息,例如哪些是数组,哪些是切片,哪些是结构体。
根据reflect.Value
的Field
相关函数,我们可以获取一个结构体内部的组成元素。由此递归,即可对数据结构进行分析。
优劣势分析
反射是runtime的,其最大优势也在于此。可以分析运行时的数据结构的内存状态。他仅利用go语言自带的反射包即可完成功能。
其弱点在于侵入性较强,需要import
对应的包,编译那个包里的全部内容后,才可进行分析,在静态分析场景,较不灵活。
结果的呈现形式
分析结果最终要可视化,以对人友好的方式展现。
可采用html方式展示,或者通过csv格式,这两种都是简单易编写的方式。
实验
构造如下数据结构。
package astruct
type (
A struct {
Bb []bstruct.B `fixType:"optional"`
Cc []cstruct.C `fixType:"var 2"`
Ee []D `fixType:"var 2"`
Dd []D `fixType:"optional"`
Ss string `fixType:"var 10"`
BtFix []byte `fixType:"fix 11"`
BtVar []byte `fixType:"var 12"`
}
D struct {
}
)
package bstruct
type B struct {
Ee []byte `fixType:"var 4"`
}
package cstruct
type C struct {
StrC []string `fixType:"var 5"`
Dd []uint32 `fixType:"fix 6"`
}
最终实验结果
astruct.A astruct.A: {160}
---|Bb []bstruct.B: {24}
---|---|bstruct.B bstruct.B: {24}
---|---|---|Ee []uint8: {24}
---|---|---|---|uint8 uint8: {1}
---|Cc []cstruct.C: {24}
---|---|cstruct.C cstruct.C: {48}
---|---|---|StrC []string: {24}
---|---|---|---|string string: {16}
---|---|---|Dd []uint32: {24}
---|---|---|---|uint32 uint32: {4}
---|Ee []astruct.D: {24}
---|---|astruct.D astruct.D: {0}
---|Dd []astruct.D: {24}
---|---|astruct.D astruct.D: {0}
---|Ss string: {16}
---|BtFix []uint8: {24}
---|---|uint8 uint8: {1}
---|BtVar []uint8: {24}
---|---|uint8 uint8: {1}