go语言111

115 阅读2分钟

利用代码生成自动注入Trace函数

要实现向目标代码中的函数/方法自动注入Trace函数,我们首先要做的就是将上面Trace函数相关的代码打包到一个module中以方便其他module导入。下面我们就先来看看将Trace函数放入一个独立的module中的步骤。

将Trace函数放入一个独立的module中

我们创建一个名为instrument_trace的目录,进入这个目录后,通过go mod init命令创建一个名为github.com/bigwhite/instrument_trace的module:

$mkdir instrument_trace
$cd instrument_trace
$go mod init github.com/bigwhite/instrument_trace
go: creating new go.mod: module github.com/bigwhite/instrument_trace

接下来,我们将最新版的trace.go放入到该目录下,将包名改为trace,并仅保留Trace函数、Trace使用的函数以及包级变量,其他函数一律删除掉。这样,一个独立的trace包就提取完毕了。

作为trace包的作者,我们有义务告诉大家如何使用trace包。 在Go中,通常我们会用一个example_test.go文件来编写使用trace包的演示代码,下面就是我们为trace包提供的example_test.go文件:

// instrument_trace/example_test.go
package trace_test
  
import (
    trace "github.com/bigwhite/instrument_trace"
)

func a() {
    defer trace.Trace()()
    b()
}

func b() {
    defer trace.Trace()()
    c()
}

func c() {
    defer trace.Trace()()
    d()
}

func d() {
    defer trace.Trace()()
}

func ExampleTrace() {
    a()
    // Output:
    // g[00001]:    ->github.com/bigwhite/instrument_trace_test.a
    // g[00001]:        ->github.com/bigwhite/instrument_trace_test.b
    // g[00001]:            ->github.com/bigwhite/instrument_trace_test.c
    // g[00001]:                ->github.com/bigwhite/instrument_trace_test.d
    // g[00001]:                <-github.com/bigwhite/instrument_trace_test.d
    // g[00001]:            <-github.com/bigwhite/instrument_trace_test.c
    // g[00001]:        <-github.com/bigwhite/instrument_trace_test.b
    // g[00001]:    <-github.com/bigwhite/instrument_trace_test.a
}

在example_test.go文件中,我们用ExampleXXX形式的函数表示一个示例,go test命令会扫描example_test.go中的以Example为前缀的函数并执行这些函数。

每个ExampleXXX函数需要包含预期的输出,就像上面ExampleTrace函数尾部那样,我们在一大段注释中提供这个函数执行后的预期输出,预期输出的内容从// Output:的下一行开始。go test会将ExampleTrace的输出与预期输出对比,如果不一致,会报测试错误。从这一点,我们可以看出example_test.go也是trace包单元测试的一部分。

现在Trace函数已经被放入到独立的包中了,接下来我们就来看看如何将它自动注入到要跟踪的函数中去。

自动注入Trace函数

现在,我们在instrument_trace module下面增加一个命令行工具,这个工具可以以一个Go源文件为单位,自动向这个Go源文件中的所有函数注入Trace函数。

我们再根据05讲中介绍的带有可执行文件的Go项目布局,在instrument_trace module中增加cmd/instrument目录,这个工具的main包就放在这个目录下,而真正实现自动注入Trace函数的代码呢,被我们放在了instrumenter目录下。

下面是变化后的instrument_trace module的目录结构:

$tree ./instrument_trace -F
./instrument_trace
├── Makefile
├── cmd/
│   └── instrument/
│       └── main.go  # instrument命令行工具的main包
├── example_test.go
├── go.mod
├── go.sum
├── instrumenter/    # 自动注入逻辑的相关结构
│   ├── ast/
│   │   └── ast.go
│   └── instrumenter.go
└── trace.go

我们先来看一下cmd/instrument/main.go源码,然后自上而下沿着main函数的调用逻辑逐一看一下这个功能的实现。下面是main.go的源码:

//  instrument_trace/cmd/instrument/main.go

... ...

var (
    wrote bool
)

func init() {
    flag.BoolVar(&wrote, "w", false, "write result to (source) file instead of stdout")
}

func usage() {
    fmt.Println("instrument [-w] xxx.go")
    flag.PrintDefaults()
}

func main() {
    fmt.Println(os.Args)
    flag.Usage = usage
    flag.Parse() // 解析命令行参数

    if len(os.Args) < 2 { // 对命令行参数个数进行校验
        usage()
        return
    }

    var file string
    if len(os.Args) == 3 {
        file = os.Args[2]
    }

    if len(os.Args) == 2 {
        file = os.Args[1]
    }
    if filepath.Ext(file) != ".go" { // 对源文件扩展名进行校验
        usage()
        return
    }

    var ins instrumenter.Instrumenter // 声明instrumenter.Instrumenter接口类型变量
    
    // 创建以ast方式实现Instrumenter接口的ast.instrumenter实例
    ins = ast.New("github.com/bigwhite/instrument_trace", "trace", "Trace") 
    newSrc, err := ins.Instrument(file) // 向Go源文件所有函数注入Trace函数
    if err != nil {
        panic(err)
    }

    if newSrc == nil {
        // add nothing to the source file. no change
        fmt.Printf("no trace added for %s\n", file)
        return
    }

    if !wrote {
        fmt.Println(string(newSrc))  // 将生成的新代码内容输出到stdout上
        return
    }

    // 将生成的新代码内容写回原Go源文件
    if err = ioutil.WriteFile(file, newSrc, 0666); err != nil {
        fmt.Printf("write %s error: %v\n", file, err)
        return
    }
    fmt.Printf("instrument trace for %s ok\n", file)
}

作为命令行工具,instrument使用标准库的flag包实现对命令行参数(这里是-w)的解析,通过os.Args获取待注入的Go源文件路径。在完成对命令行参数个数与值的校验后,instrument程序声明了一个instrumenter.Instrumenter接口类型变量ins,然后创建了一个实现了Instrumenter接口类型的ast.instrumenter类型的实例,并赋值给变量ins。

instrumenter.Instrumenter接口类型的声明放在了instrumenter/instrumenter.go中: