下面我们进入源代码来分析goc的具体实现,它的入口在goc.go文件里,是用来cobra的命令解析方式。
代码语言:javascript复制 cmd.Execute()
然后会启动一个http代理服务cmd/server.go
代码语言:javascript复制server, err := cover.NewFileBasedServer(localPersistence)
server.Run(port)
然后启用一个多路writer,分别写入文件和标准输出:
代码语言:javascript复制func (s *server) Run(port string) {
f, err := os.Create(LogFile)
mw := io.MultiWriter(f, os.Stdout)
r := s.Route(mw)
log.Fatal(r.Run(port))
然后基于gin框架,启动一个http服务
代码语言:javascript复制 func (s *server) Route(w io.Writer) *gin.Engine {
r := gin.Default()
r.StaticFile("static", "./" s.PersistenceFile)
v1 := r.Group("/v1")
{
v1.POST("/cover/register", s.registerService)
v1.GET("/cover/profile", s.profile)
v1.POST("/cover/profile", s.profile)
v1.POST("/cover/clear", s.clear)
v1.POST("/cover/init", s.initSystem)
v1.GET("/cover/list", s.listServices)
v1.POST("/cover/remove", s.removeServices)
}
以服务注册为例,它类似于服务发现,用来把带桩的server注册到到goc服务上
代码语言:javascript复制func (s *server) registerService(c *gin.Context) {
var service ServiceUnderTest
if err := c.ShouldBind(&service); err != nil {
u, err := url.Parse(service.Address)
service.Address = fmt.Sprintf("%s://%s", u.Scheme, host)
address := s.Store.Get(service.Name)
if !contains(address, service.Address) {
if err := s.Store.Add(service);
然后就可以看到进行性能分析的具体实现
代码语言:javascript复制func (s *server) profile(c *gin.Context) {
if err := c.ShouldBind(&body); err != nil {
allInfos := s.Store.GetAll()
filterAddrInfoList, err := filterAddrInfo(body.Service, body.Address, body.Force, allInfos)
for _, addrInfo := range filterAddrInfoList {
pp, err := NewWorker(addrInfo.Address).Profile(ProfileParam{})
profile, err := convertProfile(pp)
mergedProfiles = append(mergedProfiles, profile)
merged, err := cov.MergeMultipleProfiles(mergedProfiles)
if err := cov.DumpProfile(merged, c.Writer); err != nil {
先拿到全量地址,然后过滤出我们需要的的地址,然后向对应地址发送请求,获取该服务的覆盖率信息。本质上是一个代理,解耦了被检测的服务和goc server,发起代理请求的代码实现位于:pkg/cover/client.go
代码语言:javascript复制func NewWorker(host string) Action {
_, err := url.ParseRequestURI(host)
client: http.DefaultClient,
代码语言:javascript复制func (c *client) Profile(param ProfileParam) ([]byte, error) {
u := fmt.Sprintf("%s%s", c.Host, CoverProfileAPI)
res, profile, err := c.do("POST", u, "application/json", bytes.NewReader(body))
分析完提供覆盖率服务的过程,我们分析下编译出带桩二进制代码的过程cmd/build.go
代码语言:javascript复制 wd, err := os.Getwd()
runBuild(args, wd)
先构建出编译需要的环境,然后进行打桩代码的插入,最后进行编译:
代码语言:javascript复制func runBuild(args []string, wd string) {
gocBuild, err := build.NewBuild(buildFlags, args, wd, buildOutput)
ci := &cover.CoverInfo{
err = cover.Execute(ci)
err = gocBuild.Build()
pkg/build/build.go
代码语言:javascript复制func NewBuild(buildflags string, args []string, workingDir string, outputDir string) (*Build, error) {
b := &Build{
if false == b.validatePackageForBuild() {
b.MvProjectsToTmp()
dir, err := b.determineOutputDir(outputDir)
编译的过程并不会在原来的目录,而是会在一个临时目录里进行,否则和以前的源码是会有冲突的。
代码语言:javascript复制b.Pkgs, err = cover.ListPackages(b.WorkingDir, strings.Join(listArgs, " "), "")
err = b.mvProjectsToTmp()
b.OriGOPATH = os.Getenv("GOPATH")
b.NewGOPATH = fmt.Sprintf("%v:%v", b.TmpDir, b.OriGOPATH)
首先是获取项目里所有的包名,其实是调用了go list命令
代码语言:javascript复制cmd := exec.Command("/bin/bash", "-c", "go list " args)
然后进行路径的转换操作
代码语言:javascript复制b.TmpDir = filepath.Join(os.TempDir(), tmpFolderName(b.WorkingDir))
os.RemoveAll(b.TmpDir)
b.GlobalCoverVarImportPath = filepath.Join("src", tmpPackageName(b.WorkingDir))
err := os.MkdirAll(filepath.Join(b.TmpDir, b.GlobalCoverVarImportPath), os.ModePerm)
b.IsMod, b.Root, err = b.traversePkgsList()
b.TmpWorkingDir, err = b.getTmpwd()
b.cpGoModulesProject()
updated, newGoModContent, err := b.updateGoModFile()
最后更新go mod文件
代码语言:javascript复制tempModfile := filepath.Join(b.TmpDir, "go.mod")
buf, err := ioutil.ReadFile(tempModfile)
紧接着就会进行编译操作,就是调用go build
代码语言:javascript复制func (b *Build) Build() error {
cmd := exec.Command("/bin/bash", "-c", "go build " b.BuildFlags " " b.Packages)
比较核心的是pkg/cover/cover.go的Execute方法
代码语言:javascript复制func Execute(coverInfo *CoverInfo) error {
globalCoverVarImportPath = filepath.Join(coverInfo.ModRootPath, globalCoverVarImportPath)
pkgs, err := ListPackages(target, strings.Join(listArgs, " "), newGopath)
for _, pkg := range pkgs {
if pkg.Name == "main" {
mainCover, mainDecl := AddCounters(pkg, mode, globalCoverVarImportPath)
for _, dep := range pkg.Deps {
packageCover, depDecl := AddCounters(depPkg, mode, globalCoverVarImportPath)
var httpCoverApis = fmt.Sprintf("%s/http_cover_apis_auto_generated.go", pkg.Dir)
if err := InjectCountersHandlers(tc, httpCoverApis); err != nil {
return injectGlobalCoverVarFile(coverInfo, allDecl)
获取所有包名
代码语言:javascript复制 cmd := exec.Command("/bin/bash", "-c", "go list " args)
对main包,和它依赖的包分别加上counter
代码语言:javascript复制func AddCounters(pkg *Package, mode string, globalCoverVarImportPath string) (*PackageCover, string) {
coverVarMap := declareCoverVars(pkg)
Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
for file, coverVar := range coverVarMap {
decl = "n" tool.Annotate(path.Join(pkg.Dir, file), mode, coverVar.Var, globalCoverVarImportPath) "n"
switch mode {
case "set":
counterStmt = setCounterStmt
case "count":
counterStmt = incCounterStmt
case "atomic":
counterStmt = atomicCounterStmt
default:
counterStmt = incCounterStmt
}
parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments)
ast.Walk(file, file.astFile)
核心原理就是解析文件源码得到抽象语法书,然后遍历语法树,加上对应的打桩代码。最后一步是加下import文件
代码语言:javascript复制packageName := "package " filepath.Base(ci.GlobalCoverVarImportPath) "nn"
packageName := "package " filepath.Base(ci.GlobalCoverVarImportPath) "nn"
为了和官方实现做对比,有相关的单测:
代码语言:javascript复制func buildCoverCmd(file string, coverVar *FileVar, pkg *Package, mode, newgopath string) *exec.Cmd {
go tool cover -mode=atomic -o dest src (note: dest==src)
cmd := exec.Command("go", newArgs...)
pkg/cover/cover_test.go
代码语言:javascript复制func TestBuildCoverCmd(t *testing.T) {
cmd := buildCoverCmd(testCase.file, testCase.coverVar, testCase.pkg, testCase.mode, testCase.newgopath)
if !reflect.DeepEqual(cmd, testCase.expectCmd) {
代码插桩的逻辑,核心实现是访问者模式pkg/cover/internal/tool/cover.go
代码语言:javascript复制func (f *File) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.BlockStmt:
case *ast.CaseClause: // switch
for _, n := range n.List {
clause := n.(*ast.CaseClause)
f.addCounters(clause.Colon 1, clause.Colon 1, clause.End(), clause.Body, false)
case *ast.CommClause: // select
f.addCounters(n.Lbrace, n.Lbrace 1, n.Rbrace 1, n.List, true)
case *ast.IfStmt:
ast.Walk(f, n.Init)
ast.Walk(f, n.Cond)
ast.Walk(f, n.Body)
对于每一个逻辑分支,添加打桩代码
代码语言:javascript复制func (f *File) addCounters(pos, insertPos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) {
代码语言:javascript复制func Annotate(name string, mode string, varVar string, globalCoverVarImportPath string) string {
对于查询覆盖率,逻辑是一样的cmd/cover.go
代码语言:javascript复制 runCover(target)
_ = cover.Execute(ci)
源码实现的同时也实现了对应的的vscode插件,首先可以看下它的配置
tools/vscode-ext/package.json
代码语言:javascript复制"configuration": {
"title": "Goc",
"properties": {
"goc.serverUrl": {
"type": "string",
"default": "http://127.0.0.1:7777",
"description": "Specify the goc server url."
},
"goc.debug": {
"type": "boolean",
"default": false,
"description": "Turn on debug mode to log more details."
}
}
}
对应入口文件是:tools/vscode-ext/src/extension.ts,会检查环境然后执行覆盖率收集和展示:
代码语言:javascript复制let err = gocserver.checkGoEnv()
let packages = gocserver.getGoList();
await gocserver.startQueryLoop(packages);
核心逻辑位于tools/vscode-ext/src/gocserver.ts,首先是编辑器上覆盖率展示的代码:
代码语言:javascript复制private highlightDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: 'green',
border: '2px solid white',
color: 'white'
});;
代码语言:javascript复制async startQueryLoop(packages: any[]) {
this.getConfigurations();
this.setDebugLogger();
let profile = await this.getLatestProfile();
this.renderFile(packages, profile);
代码语言:javascript复制clearHightlight() {
vscode.window.visibleTextEditors.forEach(visibleEditor => {
visibleEditor.setDecorations(this.highlightDecorationType, []);
});
获取最新覆盖率,其实是发起了一个http请求去查询最新的覆盖率信息:
代码语言:javascript复制 async getLatestProfile(): Promise<string> {
let profileApi = `${this._serverUrl}/v1/cover/profile?force=true`;
let res = await axios.get(profileApi, );
let body: string = res.data.toString();
this._logger.debug(body);
代码语言:javascript复制checkGoEnv() : Boolean {
let output = spawnSync('go', ['version']);
根据覆盖率信息,更新代码的展示样式:
代码语言:javascript复制getGoList(): Array<any> {
let output = spawnSync('go', ['list', '-json', './...'], opts);
let packages = JSON.parse('[' output.stdout.toString().replace(/}n{/g, '},n{') ']');
renderFile(packages: Array<any>, profile: string) {
for (let i=0; i<packages.length; i ) {
this.triggerUpdateDecoration(ranges);
triggerUpdateDecoration(ranges: vscode.Range[]) {
vscode.window.activeTextEditor.setDecorations(
this.highlightDecorationType,
ranges
)