golang数据结构可视化分析

solution of golang数据结构分析

问题来源

工作中我们经常需要对数据结构进行各种维度的分析,例如,层次结构、静态规格、运行时内存大小、拥有的函数、实现的接口等等。

作者前段时间在进行数据库的定长化改造,其中涉及到需要对go数据结构的静态规格分析,下面将介绍两种可行的方案。

go的背景介绍

在进行分析前,先简单介绍下golang的特点,这对“用go的办法解决go的问题”有一定的引导作用,同时也是一种约束。

  • go是一种需要编译才能运行的编程语言。
  • go有比较严格的类型检查,拥有interface机制,拥有较为强大的反射机制,但缺少泛型机制。
  • go的设计思路:简单即复杂,用简单的语法表达复杂的逻辑。

数据结构分析方案一——golang编译前端之AST

AST即抽象语法树。

阅读资料

go 语言设计与实现 第二章 编译原理

golang深入源代码系列之一:AST的遍历

golang深入源代码系列之二:反向调用关系的生成

Go的AST(抽象语法树)

golang提供的静态编译工具链:

golang.org/x/tools/go/loader

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.

golang.org/x/tools/go/pointer

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.

golang.org/x/tools/go/ssa

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可以理解为一个代码元素,分为几组。

  1. 特殊token。例如,ILLEGAL(非法TOKEN)、EOF(文件末尾)、COMMENT(注释)
  2. 字面token。例如,标识符IDENT、数字INT、字符串STRING等等。
  3. 操作符token+ - * / , . ; ( )等等。
  4. 关键字tokenvar,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反射特性

阅读资料

Go 语言设计与实现 4.3 反射

反射三定律

反射的简单解释

反射是一种程序能够检查其自身结构的能力,尤其是通过类型信息。这是元编程的一种形式。它建立在golang的类型系统上。

可行性分析

反射是基于类型系统的,而我们要进行的正是类型分析。我们可以通过reflect.TypeKind枚举得到我们关心的信息,例如哪些是数组,哪些是切片,哪些是结构体。

根据reflect.ValueField相关函数,我们可以获取一个结构体内部的组成元素。由此递归,即可对数据结构进行分析。

优劣势分析

反射是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}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值