亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

一、引入

defer 是 Gopher 們都喜歡的語言機制,除了捕捉 panic、延遲釋放資源外,我們日常編碼中還有哪些使用 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ù)調用跟蹤

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

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

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

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

我們以 foo 函數(shù)中的 defer Trace("foo")() 這行代碼為例,Go 會對 defer 后面的表達式 Trace("foo")() 進行求值。由于這個表達式包含一個函數(shù)調用 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ù)調用鏈的實現(xiàn)原理后,我們再來看看這個實現(xiàn)。我們會發(fā)現(xiàn)這里還是有一些“瑕疵”,也就是離我們期望的“跟蹤函數(shù)調用鏈”的實現(xiàn)還有一些不足之處。這里我列舉了幾點:

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

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

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

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

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

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

defer Trace("foo")()

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

defer Trace()()

這種一致的 Trace 函數(shù)調用方式也為后續(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ù)調用棧上的信息,runtime.Caller 的參數(shù)標識的是要獲取的是哪一個棧幀的信息。當參數(shù)為 0 時,返回的是 Caller 函數(shù)的調用者的函數(shù)信息,在這里就是 Trace 函數(shù)。但我們需要的是 Trace 函數(shù)的調用者的信息,于是我們傳入 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ù)調用鏈的出入口信息輸出就會混雜在一起,無法分辨。

那么,接下來我們還繼續(xù)對 Trace 函數(shù)進行改造,讓它支持多 Goroutine 函數(shù)調用鏈的跟蹤。我們的方案就是在輸出的函數(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ù)值范圍,也可以自行調整控制字符串。

接下來,我們也要對示例進行一些調整,將這個程序由單 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 的調用鏈為 A1 -> B1 -> C1 -> D,而另外一個 Goroutine 的函數(shù)調用鏈為 A2 -> B2 -> C2 -> D。我們來看一下這個程序的執(zhí)行結果是否和原代碼中兩個 Goroutine 的調用鏈一致:

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

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

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

對于程序員來說,縮進是最能體現(xiàn)出“層次感”的方法,如果我們將上面示例中 Goroutine 00001 的函數(shù)調用跟蹤信息以下面的形式展示出來,函數(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

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

// 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ù)調用,我們就在輸出入口跟蹤信息之前,將縮進層次加一,并輸出入口跟蹤信息,加一后的縮進層次值也保存到 map 中。然后,在函數(shù)退出前,我們取出當前縮進層次值并輸出出口跟蹤信息,之后再將縮進層次減一后保存到 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ù)調用跟蹤信息,我們可以更容易地識別某個 Goroutine 的函數(shù)調用關系。

到這里,我們的函數(shù)調用鏈跟蹤已經(jīng)支持了多 Goroutine,并且可以輸出有層次感的跟蹤信息了,但對于 Trace 特性的使用者而言,他們依然需要手工在自己的函數(shù)中添加對 Trace 函數(shù)的調用。那么我們是否可以將 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í)行后的預期輸出,預期輸出的內容從 // 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ù)的調用邏輯逐一看一下這個功能的實現(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))  // 將生成的新代碼內容輸出到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 包實現(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ù)的新源文件內容以及一個 error 類型值,作為錯誤狀態(tài)標識。我們之所以要抽象出一個接口類型,考慮的就是注入 Trace 函數(shù)的實現(xiàn)方法不一,為后續(xù)的擴展做好預留。

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

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

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

一旦我們通過 ast 相關包解析 Go 源碼得到相應的抽象語法樹后,我們便可以操作這棵語法樹,并按我們的邏輯在語法樹中注入我們的 Trace 函數(shù),最后我們再將修改后的抽象語法樹轉換為 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轉換回Go源碼
    if err != nil {
        return nil, fmt.Errorf("error formatting new code: %w", err)
    }
    return buf.Bytes(), nil // 返回轉換后的Go源碼
}

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

實際的注入操作發(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ù)聲明,就調用 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 重新轉換為 Go 代碼,這就是我們期望得到的帶有 Trace 特性的 Go 代碼了。

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

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

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

// 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 的內容將保持不變。

由于 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ù)調用鏈的實現(xiàn)的文章就介紹到這了,更多相關Go跟蹤函數(shù)調用鏈內容請搜索腳本之家以前的文章或繼續(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是一門支持指針的編程語言,指針是一種特殊的變量,存儲了其他變量的地址。通過指針,可以在程序中直接訪問和修改變量的值,避免了不必要的內存拷貝和傳遞。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

最新評論