WHAT(Go代码调用链路可视化工具是什么?)
与链路追踪(Tracing)不同,Tracing关注复杂的分布式环境中各个服务节点间的调用关系,主要用于服务治理。而我们本次探索的代码调用链路则是代码方法级别的调用关系,主要用于代码设计。
可视化工具可以将代码间的调用关系通过图表的方式展示出来,如下图(来自go-callvis工具)
WHY
将代码间调用关系可视化后,我们可以更快速的了解代码逻辑。
尤其是在进行代码重构时,使用工具能让我们对代码有一个整体的了解,而不是人工一行行代码去梳理。
HOW(如何生成调用关系图)
go-callvis是github上开源的一个工具
1
2
|
此工具的目的是使用调用图中的数据及其与包和类型的关系,为开发人员提供Go程序的可视化概述。
这在代码复杂度更高的大型项目中尤其有用,或者您只是试图理解其他人的代码。
|
下图是go-callvis自己的调用关系:
go-callvis除了可以生成图片文件,还可以生成svg图,它默认会启动一个Web Server,我们可以在浏览器访问它的地址,在页面上实现交互式的浏览调用关系。
SVG:
1
2
|
SVG是一种用XML定义的语言,用来描述二维矢量及矢量/栅格图形。
SVG图形是可交互的和动态的,可以在SVG文件中嵌入动画元素或通过脚本来定义动画。
|
下面是一段SVG代码:
1
2
3
|
<g id="a_clust3"><a xlink:href="/?f=github.com/goccy/go-graphviz/cgraph" xlink:title="package: github.com/goccy/go-graphviz/cgraph">
<polygon fill="#ffffe0" stroke="#000000" stroke-width=".8" points="861.8909,-442.8 861.8909,-521.8 972.5803,-521.8 972.5803,-442.8 861.8909,-442.8"/>
<text text-anchor="middle" x="917.2356" y="-503.4" font-family="Tahoma bold" font-size="16.00" fill="#000000">cgraph</text>
|
可以看到和HTML类似,同样是一种标记语言。
go-callvis使用介绍
首先使用go get -u github.com/ofabry/go-callvis
命令进行安装,安装完成后go-callvis将出现在你得GOPATH/bin目录下。
命令行参数解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
go-callvis: visualize call graph of a Go program.
Usage:
go-callvis [flags] package //package即想要进行分析的包名,注意:package必须是main包或者包含单元测试的包,原因稍候介绍
Flags:
-cacheDir string
如果指定了缓存目录,生成过的图片将被保存下来,后续使用时不需要再渲染
-debug
开启调试日志.
-file string
指定输出文件名,使用后将不在启动Web Server
-focus string
定位到指定的package,package可以是包名也可以是包的import路径
-format string
指定输出文件格式[svg | png | jpg | ...] (default "svg")
-graphviz
使用本地安装的graphviz的dot命令,否则使用graphviz的go库
-group string
分组方式: packages and/or types [pkg, type] (separated by comma) (default "pkg")
-http string
Web Server地址. (default ":7878")
-ignore string
忽略的packages,多个使用逗号分隔。(使用前缀匹配)
-include string
必须包含的packages,多个使用逗号分隔。优先级比ignore和limit高(使用前缀匹配)
-limit string
限制的packages,多个使用逗号分隔(使用前缀匹配)
-minlen uint
两个节点直接最小连线长度(用于更宽的输出). (default 2)
-nodesep float
同一列中两个相邻节点之间的最小空间(用于更高的输出). (default 0.35)
-nodeshape string
节点形状 (查看graphvis文档,获取更多可用值) (default "box")
-nodestyle string
节点style(查看graphvis文档,获取更多可用值) (default "filled,rounded")
-nointer
忽略未导出的方法
-nostd
忽略标准库的方法
-rankdir string
对齐方式 [LR 调用关系从左到右| RL 从右到左| TB 从上到下| BT 从下到上] (default "LR")
-skipbrowser
不打开浏览器
-tags build tags
支持传入build tags
-tests
包含测试代码
-version
Show version and exit.
|
使用示例
1. 最简单的命令如下:
此命令会在当前目录进行分析,如果没有错误,会自动打开浏览器,在浏览器中展示图
2. 指定package
1
|
go-callvis github.com/ofabry/go-callvis
|
指定的package是main,工具将以main方法作为起始点进行链路生成
3. 指定包含单元测试方法的package
1
|
go-callvis -tests yourpackage
|
如果不想从main方法开始,可以使用-tests参数,在想要进行链路生成的package下面创建一个单元测试方法,测试方法中调用你想要作为起始点的方法。
4. 输出结果到文件
以上都是打开浏览器进行交互式浏览和操作,如果只要输出文件,可以使用-file参数
1
|
go-callvis -file yourfilename -format png yourpackage
|
5. include、limit、ignore参数
这三个参数用来控制过滤哪些调用关系(pkg1.FuncA -> pkg2.FuncB,形成一条调用关系,pkg1.FuncA为caller,pkg2.FuncB为callee)。例如代码中频繁出现的log包方法调用,没必要输出到链路中。可以使用ignore参数进行过滤
1
|
go-callvis -ignore yourlogpkg yourpackage
|
- 当调用关系中caller的pkg或者callee的pkg有任意一个在include中,则这条关系被保留。
- 不满足1时,当调用关系中caller的pkg或者callee的pkg有任意一个不在limit中,则这条关系被过滤。
- 不满足1时,当调用关系中caller的pkg或者callee的pkg有任意一个在ignore中,则这条关系被过滤。
6. 过滤标准库
过滤掉代码中频繁使用的标准库方法调用,例如:fmt、math、strings等
1
|
go-callvis -nostd yourpackage
|
go build命令可以允许我们传入-tags参数,来控制编译的版本
例如有两个配置文件dev_config.go和release_config.go,内容分别为
dev_config.go
1
2
3
4
5
|
// +build dev
package main
var version = "DEV"
|
release_config.go
1
2
3
4
5
6
|
// +build release
package main
const version = "RELEASE"
|
每个文件都有一个编译选项(+build),编译器会根据-tags传入的参数识别应该编译哪一个文件。从而达到区分环境的效果。
go-callvis的tags参数同理。
HOW IT WORKS(go-callvis是怎么工作的)
从go-callvis的调用关系中看到,它引用了如下几个package:
1
2
3
4
5
6
7
8
|
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/pointer"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/buildutil"
"github.com/goccy/go-graphviz"
"github.com/pkg/browser"
|
其中buildutil用来接收-tags参数,browser用来打开浏览器,go-graphviz是graphviz的Go bindings实现版本。
Graphviz
1
2
3
|
Graphviz是开源的图形可视化软件。
图形可视化是一种将结构信息表示为抽象图形和网络图的方法。
它在网络、生物信息学、软件工程、数据库和网页设计、机器学习以及其他技术领域的可视化界面中有着重要的应用。
|
Go的PProf使用的可视化工具就是Graphviz,在浏览器上打开的PProf图形页面需要本地安装Graphviz。
SSA
packages、ssautil用来读取go源码并解析成相应的SSA中间代码
1
2
|
包ssa定义了Go程序元素(包、类型、函数、变量和常量)的表示
使用函数体的静态单赋值(ssa)形式中间代码(IR)
|
Pointer
golang.org/x/tools/go/pointer
包负责解析输入的ssa中间代码,生成代码调用关系
CallGraph
golang.org/x/tools/go/callgraph
包的Graph是Pointer生成结果后,生成的调用图数据,Graph结构如下
1
2
3
4
5
6
7
8
9
10
|
type Graph struct {
Root *Node // the distinguished root node
Nodes map[*ssa.Function]*Node // all nodes by function
}
type Node struct {
Func *ssa.Function // the function this node represents
ID int // 0-based sequence number
In []*Edge // unordered set of incoming call edges (n.In[*].Callee == n)
Out []*Edge // unordered set of outgoing call edges (n.Out[*].Caller == n)
}
|
示例代码
使用pointer解析方法调用关系示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
package main
import (
"flag"
"fmt"
"go/build"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/pointer"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)
func main() {
flag.Parse()
//生成Go Packages
cfg := &packages.Config{Mode: packages.LoadAllSyntax}
pkgs, err := packages.Load(cfg, flag.Args()...)
if err != nil {
fmt.Fprintf(os.Stderr, "load: %v\n", err)
os.Exit(1)
}
if packages.PrintErrors(pkgs) > 0 {
os.Exit(1)
}
//生成ssa
prog, pkgs1 := ssautil.AllPackages(pkgs, 0)
prog.Build()
//找出main package
mains, err := mainPackages(pkgs1)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
//使用pointer生成调用链路
config := &pointer.Config{
Mains: mains,
BuildCallGraph: true,
}
result, err := pointer.Analyze(config)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
//遍历调用链路
callgraph.GraphVisitEdges(result.CallGraph, func(edge *callgraph.Edge) error {
//过滤非源代码
if isSynthetic(edge) {
return nil
}
caller := edge.Caller
callee := edge.Callee
//过滤标准库代码
if inStd(caller) || inStd(callee) {
return nil
}
//过滤其他package
limits := []string{"github/erberry/test"}
if !inLimits(caller, limits) || !inLimits(callee, limits) {
return nil
}
posCaller := prog.Fset.Position(caller.Func.Pos())
filenameCaller := filepath.Base(posCaller.Filename)
//输出调用信息
fmt.Fprintf(os.Stdout, "call node: %s -> %s (%s -> %s) %v\n", caller.Func.Pkg, callee.Func.Pkg, caller, callee, filenameCaller)
return nil
})
}
func mainPackages(pkgs []*ssa.Package) ([]*ssa.Package, error) {
var mains []*ssa.Package
for _, p := range pkgs {
if p != nil && p.Pkg.Name() == "main" && p.Func("main") != nil {
mains = append(mains, p)
}
}
if len(mains) == 0 {
return nil, fmt.Errorf("no main packages")
}
return mains, nil
}
func isSynthetic(edge *callgraph.Edge) bool {
return edge.Caller.Func.Pkg == nil || edge.Callee.Func.Synthetic != ""
}
func inStd(node *callgraph.Node) bool {
pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0)
return pkg.Goroot
}
func inLimits(node *callgraph.Node, limitPaths []string) bool {
pkgPath := node.Func.Pkg.Pkg.Path()
for _, p := range limitPaths {
if strings.HasPrefix(pkgPath, p) {
return true
}
}
return false
}
|
运行go run main.go github/erberry/test
,输出:
1
2
3
4
5
6
|
call node: package github/erberry/test -> package github/erberry/test (n13:github/erberry/test.main -> n22:github/erberry/test.mainPackages) main.go
call node: package github/erberry/test -> package github/erberry/test (n4571:github/erberry/test.main$1 -> n4572:github/erberry/test.isSynthetic) main.go
call node: package github/erberry/test -> package github/erberry/test (n4571:github/erberry/test.main$1 -> n4573:github/erberry/test.inStd) main.go
call node: package github/erberry/test -> package github/erberry/test (n4571:github/erberry/test.main$1 -> n4574:github/erberry/test.inLimits) main.go
call node: package github/erberry/test -> package github/erberry/test (n4571:github/erberry/test.main$1 -> n4573:github/erberry/test.inStd) main.go
call node: package github/erberry/test -> package github/erberry/test (n4571:github/erberry/test.main$1 -> n4574:github/erberry/test.inLimits) main.go
|