
golang源码的工具链中提供了可选参数coverprofile参数,可以生成覆盖率文件,方便我们快速查看,哪行代码被覆盖了,哪行代码还需要验证,但是只支持单测环境。如果我们想在运行时刻实时统计代码的覆盖率,特别是不太在意性能损失的测试环境,就无能为力了。
于是https://github.com/qiniu/goc就诞生了,它借鉴了golang官方覆盖率统计方案和部分代码,核心原理是在编译打包的时候创建一个临时目录,将源码插桩后放到临时目录,然后编译成带覆盖率的包,用户访问代码的时候,桩实时统计访问次数,然后通过websocket上报给前端,这样就可以实时查看代码的覆盖率了。下面开始研究下源码:
入口文件是goc.go,它只是注册了各种命令行参数,使用了spf13包,具体源码参考往期博客,这里不再详述。
func main() {
cmd.Execute()根命令行注册了各个具体的子命令cmd/root.go
func Execute() {
if err := rootCmd.Execute(); err != nil {
var rootCmd = &cobra.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPostRun: func(cmd *cobra.Command, args []string) {我们核心关注的覆盖率相关的命令源码位于cmd/cover.go,在代码包初始化的时候注册进去了
func init() {
rootCmd.AddCommand(coverCmd)其中变量定义如下,最终执行力runCover方法
var coverCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
runCover(target)
},它先初始化了CoverInfo结构体,组装了运行需要的上下文和参数,然后调用Execute方法:
func runCover(target string) {
ci := &cover.CoverInfo{
_ = cover.Execute(ci)结构体定义位于pkg/cover/cover.go,具体执行的方法也位于同一个包下面:
type CoverInfo struct {
Target string
GoPath string
IsMod bool
ModRootPath string
GlobalCoverVarImportPath string // path for the injected global cover var file
OneMainPackage bool
Args string
Mode string
AgentPort string
Center string
Singleton bool
}核心代码分为下面三步:1,根据参数列出需要处理的包 2,调用AddCounters进行源码打桩,将计数代码的赋值逻辑按照源码的作用域进行打桩 3,将打桩依赖的变量声明放到一个单独的包里,让各个桩代码引用。
func Execute(coverInfo *CoverInfo) error {
pkgs, err := ListPackages(target, strings.Join(listArgs, " "), newGopath)
if pkg.Name == "main" {
mainCover, mainDecl := AddCounters(pkg, mode, globalCoverVarImportPath)
return injectGlobalCoverVarFile(coverInfo, allDecl)获取需要处理的包最为简单,直接使用了go list命令,列出所有需要的包
func ListPackages(dir string, args string, newgopath string) (map[string]*Package, error) {
cmd := exec.Command("/bin/bash", "-c", "go list "+args)然后就是打桩代码,先根据作用域编号进行声明变量,然后调用Annotate进行打桩,将结果写入decl变量
func AddCounters(pkg *Package, mode string, globalCoverVarImportPath string) (*PackageCover, string) {
coverVarMap := declareCoverVars(pkg)
decl := ""
for file, coverVar := range coverVarMap {
decl += "\n" + tool.Annotate(path.Join(pkg.Dir, file), mode, coverVar.Var, globalCoverVarImportPath) + "\n"
}声明的变量都是GoCover开头的
func declareCoverVars(p *Package) map[string]*FileVar {
coverVars[file] = &FileVar{
File: longFile,
Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
}打桩代码位于pkg/cover/internal/tool/cover.go,它通过解析golang源码的语法树,然后调用edit编辑器进行代码插桩改造,最后引入声明的桩结构类型
func Annotate(name string, mode string, varVar string, globalCoverVarImportPath string) string {
fset := token.NewFileSet()
parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments)
file.edit.Insert(file.offset(file.astFile.Name.End()),
fmt.Sprintf("; import %s %q", ".", globalCoverVarImportPath))最后一步就是将生成的带桩代码写入临时目录pkg/cover/instrument.go
func injectGlobalCoverVarFile(ci *CoverInfo, content string) error {
coverFile, err := os.Create(filepath.Join(ci.Target, ci.GlobalCoverVarImportPath, "cover.go"))
_, err = coverFile.WriteString(content)类似的命令还有cmd/profile.go
var profileCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
p := cover.ProfileParam{
Force: force,
Service: svrList,
Address: addrList,
CoverFilePatterns: coverFilePatterns,
SkipFilePatterns: skipFilePatterns,
}
res, err := cover.NewWorker(center).Profile(p)以及build,因为编译的时候打桩cmd/build.go
var buildCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
runBuild(args, wd)func runBuild(args []string, wd string) {
gocBuild, err := build.NewBuild(buildFlags, args, wd, buildOutput)
defer gocBuild.Clean()
ci := &cover.CoverInfo{
err = cover.Execute(ci)
err = gocBuild.Build()在pkg/build/build.go 中完成代码的复制
func NewBuild(buildflags string, args []string, workingDir string, outputDir string) (*Build, error) {
if err := b.MvProjectsToTmp(); err != nil {
dir, err := b.determineOutputDir(outputDir)本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!