欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解Go中跟蹤函數(shù)調(diào)用鏈的實現(xiàn)

 更新時間:2023年11月07日 08:18:17   作者:賈維斯Echo  
這篇文章主要為大家詳細介紹了go如何實現(xiàn)一個自動注入跟蹤代碼,并輸出有層次感的函數(shù)調(diào)用鏈跟蹤命令行工具,感興趣的小伙伴可以跟隨小編一起學習一下

一、引入

defer 是 Gopher 們都喜歡的語言機制,除了捕捉 panic、延遲釋放資源外,我們?nèi)粘>幋a中還有哪些使用 defer 的小技巧呢?

在平時,defer 的一個常見的使用技巧就是使用 defer 可以跟蹤函數(shù)的執(zhí)行過程。這的確是很多 Go 教程在講解 defer 時也會經(jīng)常使用這個用途舉例。那么,我們具體是怎么用 defer 來實現(xiàn)函數(shù)執(zhí)行過程的跟蹤呢?這里,給出了一個最簡單的實現(xiàn):

// trace.go
package main
  
func Trace(name string) func() {
    println("enter:", name)
    return func() {
        println("exit:", name)
    }
}

func foo() {
    defer Trace("foo")()
    bar()
}

func bar() {
    defer Trace("bar")()
}

func main() {
    defer Trace("main")()
    foo()
}

我們先看一下這段代碼的執(zhí)行結果,直觀感受一下什么是函數(shù)調(diào)用跟蹤

enter: main
enter: foo
enter: bar
exit: bar
exit: foo
exit: main

我們看到,這個 Go 程序的函數(shù)調(diào)用的全過程一目了然地展現(xiàn)在了我們面前:程序按 main -> foo -> bar 的函數(shù)調(diào)用次序執(zhí)行,代碼在函數(shù)的入口與出口處分別輸出了跟蹤日志。

那這段代碼是怎么做到的呢?我們簡要分析一下。

在這段代碼中,我們在每個函數(shù)的入口處都使用 defer 設置了一個 deferred 函數(shù)。根據(jù) defer 的運作機制,Go 會在 defer 設置 deferred 函數(shù)時對 defer 后面的表達式進行求值。

我們以 foo 函數(shù)中的 defer Trace("foo")() 這行代碼為例,Go 會對 defer 后面的表達式 Trace("foo")() 進行求值。由于這個表達式包含一個函數(shù)調(diào)用 Trace("foo"),所以這個函數(shù)會被執(zhí)行。

上面的 Trace 函數(shù)只接受一個參數(shù),這個參數(shù)代表函數(shù)名。Trace 會首先打印進入某函數(shù)的日志,比如:“enter: foo”。然后返回一個閉包函數(shù),這個閉包函數(shù)一旦被執(zhí)行,就會輸出離開某函數(shù)的日志。在 foo 函數(shù)中,這個由 Trace 函數(shù)返回的閉包函數(shù)就被設置為了 deferred 函數(shù),于是當 foo 函數(shù)返回后,這個閉包函數(shù)就會被執(zhí)行,輸出 “exit: foo”的日志。

搞清楚上面跟蹤函數(shù)調(diào)用鏈的實現(xiàn)原理后,我們再來看看這個實現(xiàn)。我們會發(fā)現(xiàn)這里還是有一些“瑕疵”,也就是離我們期望的“跟蹤函數(shù)調(diào)用鏈”的實現(xiàn)還有一些不足之處。這里我列舉了幾點:

  • 調(diào)用 Trace 時需手動顯式傳入要跟蹤的函數(shù)名;
  • 如果是并發(fā)應用,不同 Goroutine 中函數(shù)鏈跟蹤混在一起無法分辨;
  • 輸出的跟蹤結果缺少層次感,調(diào)用關系不易識別;
  • 對要跟蹤的函數(shù),需手動調(diào)用 Trace 函數(shù)。

所以,本文就是逐一分析并解決上面提出的這幾點問題進行,經(jīng)過逐步地代碼演進,最終實現(xiàn)一個自動注入跟蹤代碼,并輸出有層次感的函數(shù)調(diào)用鏈跟蹤命令行工具。

我們先來解決第一個問題。

二、自動獲取所跟蹤函數(shù)的函數(shù)名

要解決“調(diào)用 Trace 時需要手動顯式傳入要跟蹤的函數(shù)名”的問題,也就是要讓我們的 Trace 函數(shù)能夠自動獲取到它跟蹤函數(shù)的函數(shù)名信息。我們以跟蹤 foo 為例,看看這樣做能給我們帶來什么好處。

在手動顯式傳入的情況下,我們需要用下面這個代碼對 foo 進行跟蹤:

defer Trace("foo")()

一旦實現(xiàn)了自動獲取函數(shù)名,所有支持函數(shù)調(diào)用鏈跟蹤的函數(shù)都只需使用下面調(diào)用形式的 Trace 函數(shù)就可以了:

defer Trace()()

這種一致的 Trace 函數(shù)調(diào)用方式也為后續(xù)的自動向代碼中注入 Trace 函數(shù)奠定了基礎。那么如何實現(xiàn) Trace 函數(shù)對它跟蹤函數(shù)名的自動獲取呢?我們需要借助 Go 標準庫 runtime 包的幫助。

這里,我給出了新版 Trace 函數(shù)的實現(xiàn)以及它的使用方法,我們先看一下:

// trace1/trace.go

func Trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    println("enter:", name)
    return func() { println("exit:", name) }
}

func foo() {
    defer Trace()()
    bar()
}

func bar() {
    defer Trace()()
}

func main() {
    defer Trace()()
    foo()
}

在這一版 Trace 函數(shù)中,我們通過 runtime.Caller 函數(shù)獲得當前 Goroutine 的函數(shù)調(diào)用棧上的信息,runtime.Caller 的參數(shù)標識的是要獲取的是哪一個棧幀的信息。當參數(shù)為 0 時,返回的是 Caller 函數(shù)的調(diào)用者的函數(shù)信息,在這里就是 Trace 函數(shù)。但我們需要的是 Trace 函數(shù)的調(diào)用者的信息,于是我們傳入 1。

Caller 函數(shù)有四個返回值:

  • 第一個返回值代表的是程序計數(shù)(pc)。
  • 第二個和第三個參數(shù)代表對應函數(shù)所在的源文件名以及所在行數(shù),這里我們暫時不需要。
  • 最后一個參數(shù)代表是否能成功獲取這些信息,如果獲取失敗,我們拋出 panic。

接下來,我們通過 runtime.FuncForPC 函數(shù)和程序計數(shù)器(PC)得到被跟蹤函數(shù)的函數(shù)名稱。我們運行一下改造后的代碼:

enter: main.main
enter: main.foo
enter: main.bar
exit: main.bar
exit: main.foo
exit: main.main

接下來,我們來解決第二個問題,也就是當程序中有多 Goroutine 時,Trace 輸出的跟蹤信息混雜在一起難以分辨的問題。

三、增加 Goroutine 標識

上面的 Trace 函數(shù)在面對只有一個 Goroutine 的時候,還是可以支撐的,但當程序中并發(fā)運行多個 Goroutine 的時候,多個函數(shù)調(diào)用鏈的出入口信息輸出就會混雜在一起,無法分辨。

那么,接下來我們還繼續(xù)對 Trace 函數(shù)進行改造,讓它支持多 Goroutine 函數(shù)調(diào)用鏈的跟蹤。我們的方案就是在輸出的函數(shù)出入口信息時,帶上一個在程序每次執(zhí)行時能唯一區(qū)分 Goroutine 的 Goroutine ID。

到這里,你可能會說,Goroutine 也沒有 ID 信息?。〉拇_如此,Go 核心團隊為了避免 Goroutine ID 的濫用,故意沒有將 Goroutine ID 暴露給開發(fā)者。但在 Go 標準庫的 h2_bundle.go 中,我們卻發(fā)現(xiàn)了一個獲取 Goroutine ID 的標準方法,看下面代碼:

// $GOROOT/src/net/http/h2_bundle.go
var http2goroutineSpace = []byte("goroutine ")

func http2curGoroutineID() uint64 {
    bp := http2littleBuf.Get().(*[]byte)
    defer http2littleBuf.Put(bp)
    b := *bp
    b = b[:runtime.Stack(b, false)]
    // Parse the 4707 out of "goroutine 4707 ["
    b = bytes.TrimPrefix(b, http2goroutineSpace)
    i := bytes.IndexByte(b, ' ')
    if i < 0 {
        panic(fmt.Sprintf("No space found in %q", b))
    }
    b = b[:i]
    n, err := http2parseUintBytes(b, 10, 64)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
    }
    return n
}

不過,由于 http2curGoroutineID 不是一個導出函數(shù),我們無法直接使用。我們可以把它復制出來改造一下:

// trace2/trace.go
var goroutineSpace = []byte("goroutine ")

func curGoroutineID() uint64 {
    b := make([]byte, 64)
    b = b[:runtime.Stack(b, false)]
    // Parse the 4707 out of "goroutine 4707 ["
    b = bytes.TrimPrefix(b, goroutineSpace)
    i := bytes.IndexByte(b, ' ')
    if i < 0 {
        panic(fmt.Sprintf("No space found in %q", b))
    }
    b = b[:i]
    n, err := strconv.ParseUint(string(b), 10, 64)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
    }
    return n
}

這里,我們改造了兩個地方。一個地方是通過直接創(chuàng)建一個 byte 切片賦值給 b,替代原 http2curGoroutineID 函數(shù)中從一個 pool 池獲取 byte 切片的方式,另外一個是使用 strconv.ParseUint 替代了原先的 http2parseUintBytes。改造后,我們就可以直接使用 curGoroutineID 函數(shù)來獲取 Goroutine 的 ID 信息了。

好,接下來,我們在 Trace 函數(shù)中添加 Goroutine ID 信息的輸出:

// trace2/trace.go
func Trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()

    gid := curGoroutineID()
    fmt.Printf("g[%05d]: enter: [%s]\n", gid, name)
    return func() { fmt.Printf("g[%05d]: exit: [%s]\n", gid, name) }
}

從上面代碼看到,我們在出入口輸出的跟蹤信息中加入了 Goroutine ID 信息,我們輸出的 Goroutine ID 為 5 位數(shù)字,如果 ID 值不足 5 位,則左補零,這一切都是 Printf 函數(shù)的格式控制字符串“%05d”幫助我們實現(xiàn)的。這樣對齊 Goroutine ID 的位數(shù),為的是輸出信息格式的一致性更好。如果你的 Go 程序中 Goroutine 的數(shù)量超過了 5 位數(shù)可以表示的數(shù)值范圍,也可以自行調(diào)整控制字符串。

接下來,我們也要對示例進行一些調(diào)整,將這個程序由單 Goroutine 改為多 Goroutine 并發(fā)的,這樣才能驗證支持多 Goroutine 的新版 Trace 函數(shù)是否好用:

// trace2/trace.go
func A1() {
    defer Trace()()
    B1()
}

func B1() {
    defer Trace()()
    C1()
}

func C1() {
    defer Trace()()
    D()
}

func D() {
    defer Trace()()
}

func A2() {
    defer Trace()()
    B2()
}
func B2() {
    defer Trace()()
    C2()
}
func C2() {
    defer Trace()()
    D()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        A2()
        wg.Done()
    }()

    A1()
    wg.Wait()
}

新示例程序共有兩個 Goroutine,main goroutine 的調(diào)用鏈為 A1 -> B1 -> C1 -> D,而另外一個 Goroutine 的函數(shù)調(diào)用鏈為 A2 -> B2 -> C2 -> D。我們來看一下這個程序的執(zhí)行結果是否和原代碼中兩個 Goroutine 的調(diào)用鏈一致:

g[00001]: enter: [main.A1]
g[00001]: enter: [main.B1]
g[00018]: enter: [main.A2]
g[00001]: enter: [main.C1]
g[00001]: enter: [main.D]
g[00001]: exit: [main.D]
g[00001]: exit: [main.C1]
g[00001]: exit: [main.B1]
g[00001]: exit: [main.A1]
g[00018]: enter: [main.B2]
g[00018]: enter: [main.C2]
g[00018]: enter: [main.D]
g[00018]: exit: [main.D]
g[00018]: exit: [main.C2]
g[00018]: exit: [main.B2]
g[00018]: exit: [main.A2]

我們看到,新示例程序輸出了帶有 Goroutine ID 的出入口跟蹤信息,通過 Goroutine ID 我們可以快速確認某一行輸出是屬于哪個 Goroutine 的。

但由于 Go 運行時對 Goroutine 調(diào)度順序的不確定性,各個 Goroutine 的輸出還是會存在交織在一起的問題,這會給你查看某個 Goroutine 的函數(shù)調(diào)用鏈跟蹤信息帶來阻礙。這里我提供一個小技巧:你可以將程序的輸出重定向到一個本地文件中,然后通過 Goroutine ID 過濾出(可使用 grep 工具)你想查看的 goroutine 的全部函數(shù)跟蹤信息。

到這里,我們就實現(xiàn)了輸出帶有 Goroutine ID 的函數(shù)跟蹤信息,不過,你是不是也覺得輸出的函數(shù)調(diào)用鏈信息還是不夠美觀,缺少層次感,體驗依舊不那么優(yōu)秀呢?至少我是這么覺得的。所以下面我們就來美化一下信息的輸出形式。

四、讓輸出的跟蹤信息更具層次感

對于程序員來說,縮進是最能體現(xiàn)出“層次感”的方法,如果我們將上面示例中 Goroutine 00001 的函數(shù)調(diào)用跟蹤信息以下面的形式展示出來,函數(shù)的調(diào)用順序是不是更加一目了然了呢?

g[00001]:    ->main.A1
g[00001]:        ->main.B1
g[00001]:            ->main.C1
g[00001]:                ->main.D
g[00001]:                <-main.D
g[00001]:            <-main.C1
g[00001]:        <-main.B1
g[00001]:    <-main.A1

那么我們就以這個形式為目標,考慮如何實現(xiàn)輸出這種帶縮進的函數(shù)調(diào)用跟蹤信息。我們還是直接上代碼吧:

// trace3/trace.go

func printTrace(id uint64, name, arrow string, indent int) {
    indents := ""
    for i := 0; i < indent; i++ {
        indents += "    "
    }
    fmt.Printf("g[%05d]:%s%s%s\n", id, indents, arrow, name)
}

var mu sync.Mutex
var m = make(map[uint64]int)

func Trace() func() {
    pc, _, _, ok := runtime.Caller(1)
    if !ok {
        panic("not found caller")
    }

    fn := runtime.FuncForPC(pc)
    name := fn.Name()
    gid := curGoroutineID()

    mu.Lock()
    indents := m[gid]    // 獲取當前gid對應的縮進層次
    m[gid] = indents + 1 // 縮進層次+1后存入map
    mu.Unlock()
    printTrace(gid, name, "->", indents+1)
    return func() {
        mu.Lock()
        indents := m[gid]    // 獲取當前gid對應的縮進層次
        m[gid] = indents - 1 // 縮進層次-1后存入map
        mu.Unlock()
        printTrace(gid, name, "<-", indents)
    }
}

在上面這段代碼中,我們使用了一個 map 類型變量 m 來保存每個 Goroutine 當前的縮進信息:m 的 key 為 Goroutine 的 ID,值為縮進的層次。然后,考慮到 Trace 函數(shù)可能在并發(fā)環(huán)境中運行,根據(jù)Go 中的“map 不支持并發(fā)寫”的特性,我們增加了一個 sync.Mutex 實例 mu 用于同步對 m 的寫操作。

這樣,對于一個 Goroutine 來說,每次剛進入一個函數(shù)調(diào)用,我們就在輸出入口跟蹤信息之前,將縮進層次加一,并輸出入口跟蹤信息,加一后的縮進層次值也保存到 map 中。然后,在函數(shù)退出前,我們?nèi)〕霎斍翱s進層次值并輸出出口跟蹤信息,之后再將縮進層次減一后保存到 map 中。

除了增加縮進層次信息外,在這一版的 Trace 函數(shù)實現(xiàn)中,我們也把輸出出入口跟蹤信息的操作提取到了一個獨立的函數(shù) printTrace 中,這個函數(shù)會根據(jù)傳入的 Goroutine ID、函數(shù)名、箭頭類型與縮進層次值,按預定的格式拼接跟蹤信息并輸出。

運行新版示例代碼,我們會得到下面的結果:

g[00001]:    ->main.A1
g[00001]:        ->main.B1
g[00001]:            ->main.C1
g[00001]:                ->main.D
g[00001]:                <-main.D
g[00001]:            <-main.C1
g[00001]:        <-main.B1
g[00001]:    <-main.A1
g[00018]:    ->main.A2
g[00018]:        ->main.B2
g[00018]:            ->main.C2
g[00018]:                ->main.D
g[00018]:                <-main.D
g[00018]:            <-main.C2
g[00018]:        <-main.B2
g[00018]:    <-main.A2

顯然,通過這種帶有縮進層次的函數(shù)調(diào)用跟蹤信息,我們可以更容易地識別某個 Goroutine 的函數(shù)調(diào)用關系。

到這里,我們的函數(shù)調(diào)用鏈跟蹤已經(jīng)支持了多 Goroutine,并且可以輸出有層次感的跟蹤信息了,但對于 Trace 特性的使用者而言,他們依然需要手工在自己的函數(shù)中添加對 Trace 函數(shù)的調(diào)用。那么我們是否可以將 Trace 特性自動注入特定項目下的各個源碼文件中呢?接下來我們繼續(xù)來改進我們的 Trace 工具。

五、利用代碼生成自動注入 Trace 函數(shù)

要實現(xiàn)向目標代碼中的函數(shù) / 方法自動注入 Trace 函數(shù),我們首先要做的就是將上面 Trace 函數(shù)相關的代碼打包到一個 module 中以方便其他 module 導入。下面我們就先來看看將 Trace 函數(shù)放入一個獨立的 module 中的步驟。

5.1 將 Trace 函數(shù)放入一個獨立的 module 中

我們創(chuàng)建一個名為 instrument_trace 的目錄,進入這個目錄后,通過 go mod init 命令創(chuàng)建一個名為 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 函數(shù)、Trace 使用的函數(shù)以及包級變量,其他函數(shù)一律刪除掉。這樣,一個獨立的 trace 包就提取完畢了。

作為 trace 包的作者,我們有義務告訴大家如何使用 trace 包。在 Go 中,通常我們會用一個 example_test.go 文件來編寫使用 trace 包的演示代碼,下面就是我們?yōu)?nbsp;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 形式的函數(shù)表示一個示例,go test 命令會掃描 example_test.go 中的以 Example 為前綴的函數(shù)并執(zhí)行這些函數(shù)。

每個 ExampleXXX 函數(shù)需要包含預期的輸出,就像上面 ExampleTrace 函數(shù)尾部那樣,我們在一大段注釋中提供這個函數(shù)執(zhí)行后的預期輸出,預期輸出的內(nèi)容從 // Output: 的下一行開始。go test 會將 ExampleTrace 的輸出與預期輸出對比,如果不一致,會報測試錯誤。從這一點,我們可以看出 example_test.go 也是 trace 包單元測試的一部分。

現(xiàn)在 Trace 函數(shù)已經(jīng)被放入到獨立的包中了,接下來我們就來看看如何將它自動注入到要跟蹤的函數(shù)中去。

5.2 自動注入 Trace 函數(shù)

現(xiàn)在,我們在 instrument_trace module 下面增加一個命令行工具,這個工具可以以一個 Go 源文件為單位,自動向這個 Go 源文件中的所有函數(shù)注入 Trace 函數(shù)。

我們再根據(jù)之前介紹的帶有【可執(zhí)行文件的 Go 項目布局】,在 instrument_trace module 中增加 cmd/instrument 目錄,這個工具的 main 包就放在這個目錄下,而真正實現(xiàn)自動注入 Trace 函數(shù)的代碼呢,被我們放在了 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 函數(shù)的調(diào)用邏輯逐一看一下這個功能的實現(xiàn)。下面是 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() // 解析命令行參數(shù)

    if len(os.Args) < 2 { // 對命令行參數(shù)個數(shù)進行校驗
        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接口類型變量
    
    // 創(chuàng)建以ast方式實現(xiàn)Instrumenter接口的ast.instrumenter實例
    ins = ast.New("github.com/bigwhite/instrument_trace", "trace", "Trace") 
    newSrc, err := ins.Instrument(file) // 向Go源文件所有函數(shù)注入Trace函數(shù)
    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))  // 將生成的新代碼內(nèi)容輸出到stdout上
        return
    }

    // 將生成的新代碼內(nèi)容寫回原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 包實現(xiàn)對命令行參數(shù)(這里是 -w)的解析,通過 os.Args 獲取待注入的 Go 源文件路徑。在完成對命令行參數(shù)個數(shù)與值的校驗后,instrument 程序聲明了一個 instrumenter.Instrumenter 接口類型變量 ins,然后創(chuàng)建了一個實現(xiàn)了 Instrumenter 接口類型的 ast.instrumenter 類型的實例,并賦值給變量 ins。

instrumenter.Instrumenter 接口類型的聲明放在了 instrumenter/instrumenter.go 中:

type Instrumenter interface {
    Instrument(string) ([]byte, error)
}

這里我們看到,這個接口類型的方法列表中只有一個方法 Instrument,這個方法接受一個 Go 源文件路徑,返回注入了 Trace 函數(shù)的新源文件內(nèi)容以及一個 error 類型值,作為錯誤狀態(tài)標識。我們之所以要抽象出一個接口類型,考慮的就是注入 Trace 函數(shù)的實現(xiàn)方法不一,為后續(xù)的擴展做好預留。

在這個例子中,我們默認提供了一種自動注入 Trace 函數(shù)的實現(xiàn),那就是 ast.instrumenter,它注入 Trace 的實現(xiàn)原理是這樣的:

從原理圖中我們可以清楚地看到,在這一實現(xiàn)方案中,我們先將傳入的 Go 源碼轉(zhuǎn)換為抽象語法樹。

在計算機科學中,抽象語法樹(abstract syntax tree,AST)是源代碼的抽象語法結構的樹狀表現(xiàn)形式,樹上的每個節(jié)點都表示源代碼中的一種結構。因為 Go 語言是開源編程語言,所以它的抽象語法樹的操作包也和語言一起開放給了 Go 開發(fā)人員,我們可以基于 Go 標準庫以及 Go 實驗工具庫 提供的 ast 相關包,快速地構建基于 AST 的應用,這里的 ast.instrumenter 就是一個應用 AST 的典型例子。

一旦我們通過 ast 相關包解析 Go 源碼得到相應的抽象語法樹后,我們便可以操作這棵語法樹,并按我們的邏輯在語法樹中注入我們的 Trace 函數(shù),最后我們再將修改后的抽象語法樹轉(zhuǎn)換為 Go 源碼,就完成了整個自動注入的工作了。

了解了原理后,我們再看一下具體的代碼實現(xiàn)。下面是 ast.instrumenter 的 Instructment 方法的代碼:

// instrument_trace/instrumenter/ast/ast.go

func (a instrumenter) Instrument(filename string) ([]byte, error) {
    fset := token.NewFileSet()
    curAST, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) // 解析Go源碼,得到AST
    if err != nil {
        return nil, fmt.Errorf("error parsing %s: %w", filename, err)
    }

    if !hasFuncDecl(curAST) { // 如果整個源碼都不包含函數(shù)聲明,則無需注入操作,直接返回。
        return nil, nil
    }

    // 在AST上添加包導入語句
    astutil.AddImport(fset, curAST, a.traceImport)

    // 向AST上的所有函數(shù)注入Trace函數(shù)
    a.addDeferTraceIntoFuncDecls(curAST)

    buf := &bytes.Buffer{}
    err = format.Node(buf, fset, curAST) // 將修改后的AST轉(zhuǎn)換回Go源碼
    if err != nil {
        return nil, fmt.Errorf("error formatting new code: %w", err)
    }
    return buf.Bytes(), nil // 返回轉(zhuǎn)換后的Go源碼
}

通過代碼,我們看到 Instrument 方法的基本步驟與上面原理圖大同小異。Instrument 首先通過 go/parser 的 ParserFile 函數(shù)對傳入的 Go 源文件中的源碼進行解析,并得到對應的抽象語法樹 AST,然后向 AST 中導入 Trace 函數(shù)所在的包,并向這個 AST 的所有函數(shù)聲明注入 Trace 函數(shù)調(diào)用。

實際的注入操作發(fā)生在 instrumenter 的 addDeferTraceIntoFuncDecls 方法中,我們來看一下這個方法的實現(xiàn):

// instrument_trace/instrumenter/ast/ast.go

func (a instrumenter) addDeferTraceIntoFuncDecls(f *ast.File) {
    for _, decl := range f.Decls { // 遍歷所有聲明語句
        fd, ok := decl.(*ast.FuncDecl) // 類型斷言:是否為函數(shù)聲明
        if ok { 
            // 如果是函數(shù)聲明,則注入跟蹤設施
            a.addDeferStmt(fd)
        }
    }
}

這個方法的邏輯十分清晰,就是遍歷語法樹上所有聲明語句,如果是函數(shù)聲明,就調(diào)用 instrumenter 的 addDeferStmt 方法進行注入,如果不是,就直接返回。addDeferStmt 方法的實現(xiàn)如下:

// instrument_trace/instrumenter/ast/ast.go

func (a instrumenter) addDeferStmt(fd *ast.FuncDecl) (added bool) {
    stmts := fd.Body.List

    // 判斷"defer trace.Trace()()"語句是否已經(jīng)存在
    for _, stmt := range stmts {
        ds, ok := stmt.(*ast.DeferStmt)
        if !ok {
            // 如果不是defer語句,則繼續(xù)for循環(huán)
            continue
        }

        // 如果是defer語句,則要進一步判斷是否是defer trace.Trace()()
        ce, ok := ds.Call.Fun.(*ast.CallExpr)
        if !ok {
            continue
        }

        se, ok := ce.Fun.(*ast.SelectorExpr)
        if !ok {
            continue
        }

        x, ok := se.X.(*ast.Ident)
        if !ok {
            continue
        }
        if (x.Name == a.tracePkg) && (se.Sel.Name == a.traceFunc) {
            // defer trace.Trace()()已存在,返回
            return false
        }
    }

    // 沒有找到"defer trace.Trace()()",注入一個新的跟蹤語句
    // 在AST上構造一個defer trace.Trace()()
    ds := &ast.DeferStmt{
        Call: &ast.CallExpr{
            Fun: &ast.CallExpr{
                Fun: &ast.SelectorExpr{
                    X: &ast.Ident{
                        Name: a.tracePkg,
                    },
                    Sel: &ast.Ident{
                        Name: a.traceFunc,
                    },
                },
            },
        },
    }

    newList := make([]ast.Stmt, len(stmts)+1)
    copy(newList[1:], stmts)
    newList[0] = ds // 注入新構造的defer語句
    fd.Body.List = newList
    return true
}

雖然 addDeferStmt 函數(shù)體略長,但邏輯也很清晰,就是先判斷函數(shù)是否已經(jīng)注入了 Trace,如果有,則略過;如果沒有,就構造一個 Trace 語句節(jié)點,并將它插入到 AST 中。

Instrument 的最后一步就是將注入 Trace 后的 AST 重新轉(zhuǎn)換為 Go 代碼,這就是我們期望得到的帶有 Trace 特性的 Go 代碼了。

5.3 利用 instrument 工具注入跟蹤代碼

有了 instrument 工具后,我們再來看看如何使用這個工具,在目標 Go 源文件中自動注入跟蹤設施。

這里,我在 instrument_trace 項目的 examples 目錄下建立了一個名為 demo 的項目,我們就來看看如何使用 instrument 工具為 demo 項目下的 demo.go 文件自動注入跟蹤設施。demo.go 文件內(nèi)容很簡單:

// instrument_trace/examples/demo/demo.go

package main

func foo() {
    bar()
}

func bar() {
}

func main() {
    foo()
}

我們首先構建一下 instrument_trace 下的 instrument 工具:

$cd instrument_trace
$go build github.com/bigwhite/instrument_trace/cmd/instrument
$instrument version 
[instrument version]
instrument [-w] xxx.go
  -w  write result to (source) file instead of stdout

接下來,我們使用 instrument 工具向 examples/demo/demo.go 源文件中的函數(shù)自動注入跟蹤設施:

$instrument -w  examples/demo/demo.go
[instrument -w examples/demo/demo.go]
instrument trace for examples/demo/demo.go ok

注入后的 demo.go 文件變?yōu)榱讼旅孢@個樣子:

// instrument_trace/examples/demo/demo.go

package main
  
import "github.com/bigwhite/instrument_trace"

func foo() {
    defer trace.Trace()()
    bar()
}

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

func main() {
    defer trace.Trace()()
    foo()
}

此時,如果我們再對已注入 Trace 函數(shù)的 demo.go 執(zhí)行一次 instrument 命令,由于 instrument 會判斷 demo.go 各個函數(shù)已經(jīng)注入了 Trace,demo.go 的內(nèi)容將保持不變。

由于 github.com/bigwhite/instrument_trace 并沒有真正上傳到 github.com 上,所以如果你要運行 demo.go,我們可以為它配置一個下面這樣的 go.mod

// instrument_trace/examples/demo/go.mod
module demo
go 1.17
require github.com/bigwhite/instrument_trace v1.0.0
replace github.com/bigwhite/instrument_trace v1.0.0 => ../../

這樣運行 demo.go 就不會遇到障礙了:

$go run demo.go
g[00001]:    ->main.main
g[00001]:        ->main.foo
g[00001]:            ->main.bar
g[00001]:            <-main.bar
g[00001]:        <-main.foo
g[00001]:    <-main.main

到此這篇關于一文帶你了解Go中跟蹤函數(shù)調(diào)用鏈的實現(xiàn)的文章就介紹到這了,更多相關Go跟蹤函數(shù)調(diào)用鏈內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Go如何優(yōu)雅的使用字節(jié)池示例詳解

    Go如何優(yōu)雅的使用字節(jié)池示例詳解

    在編程開發(fā)中,我們經(jīng)常會需要頻繁創(chuàng)建和銷毀同類對象的情形,這樣的操作很可能會對性能造成影響,這時常用的優(yōu)化手段就是使用對象池(object pool),這篇文章主要給大家介紹了關于Go如何優(yōu)雅的使用字節(jié)池的相關資料,需要的朋友可以參考下
    2022-08-08
  • go切片的copy和view的使用方法

    go切片的copy和view的使用方法

    這篇文章主要介紹了go切片的copy和view的使用方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-11-11
  • 一文教你打造一個簡易的Golang日志庫

    一文教你打造一個簡易的Golang日志庫

    這篇文章主要為大家詳細介紹了如何使用不超過130行的代碼,通過一系列golang的特性,來打造一個簡易的golang日志庫,感興趣的小伙伴可以了解一下
    2023-06-06
  • 基于Golang編寫一個聊天工具

    基于Golang編寫一個聊天工具

    這篇文章主要為大家詳細介紹了如何使用?Golang?構建一個簡單但功能完善的聊天工具,利用?WebSocket?技術實現(xiàn)即時通訊的功能,需要的小伙伴可以參考下
    2023-11-11
  • windows安裝部署go超詳細實戰(zhàn)記錄(實測有用!)

    windows安裝部署go超詳細實戰(zhàn)記錄(實測有用!)

    Golang語言在近年來因為其高性能、編譯速度快、開發(fā)成本低等特點逐漸得到大家的青睞,這篇文章主要給大家介紹了關于windows安裝部署go超詳細實戰(zhàn)的相關資料,需要的朋友可以參考下
    2023-02-02
  • Golang Cron 定時任務的實現(xiàn)示例

    Golang Cron 定時任務的實現(xiàn)示例

    這篇文章主要介紹了Golang Cron 定時任務的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-05-05
  • Golang中指針的使用詳解

    Golang中指針的使用詳解

    Golang是一門支持指針的編程語言,指針是一種特殊的變量,存儲了其他變量的地址。通過指針,可以在程序中直接訪問和修改變量的值,避免了不必要的內(nèi)存拷貝和傳遞。Golang中的指針具有高效、安全的特點,在并發(fā)編程和底層系統(tǒng)開發(fā)中得到廣泛應用
    2023-04-04
  • Go語言Slice切片底層的實現(xiàn)

    Go語言Slice切片底層的實現(xiàn)

    本文主要介紹了Go語言Slice切片底層的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-04-04
  • golang配制高性能sql.DB的使用

    golang配制高性能sql.DB的使用

    本文主要講述SetMaxOpenConns(),?SetMaxIdleConns()?和?SetConnMaxLifetime()方法,?您可以使用它們來配置sql.DB的行為并改變其性能,感興趣的可以了解一下
    2021-12-12
  • Golang實現(xiàn)Trie(前綴樹)的示例

    Golang實現(xiàn)Trie(前綴樹)的示例

    本文主要介紹了Golang實現(xiàn)Trie(前綴樹)的示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01

最新評論