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

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

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

一、引入

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

在平時(shí),defer 的一個(gè)常見的使用技巧就是使用 defer 可以跟蹤函數(shù)的執(zhí)行過程。這的確是很多 Go 教程在講解 defer 時(shí)也會(huì)經(jīng)常使用這個(gè)用途舉例。那么,我們具體是怎么用 defer 來實(shí)現(xiàn)函數(shù)執(zhí)行過程的跟蹤呢?這里,給出了一個(gè)最簡(jiǎn)單的實(shí)現(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í)行結(jié)果,直觀感受一下什么是函數(shù)調(diào)用跟蹤

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

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

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

在這段代碼中,我們?cè)诿總€(gè)函數(shù)的入口處都使用 defer 設(shè)置了一個(gè) deferred 函數(shù)。根據(jù) defer 的運(yùn)作機(jī)制,Go 會(huì)在 defer 設(shè)置 deferred 函數(shù)時(shí)對(duì) defer 后面的表達(dá)式進(jìn)行求值。

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

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

搞清楚上面跟蹤函數(shù)調(diào)用鏈的實(shí)現(xiàn)原理后,我們?cè)賮砜纯催@個(gè)實(shí)現(xiàn)。我們會(huì)發(fā)現(xiàn)這里還是有一些“瑕疵”,也就是離我們期望的“跟蹤函數(shù)調(diào)用鏈”的實(shí)現(xiàn)還有一些不足之處。這里我列舉了幾點(diǎn):

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

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

我們先來解決第一個(gè)問題。

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

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

在手動(dòng)顯式傳入的情況下,我們需要用下面這個(gè)代碼對(duì) foo 進(jìn)行跟蹤:

defer Trace("foo")()

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

defer Trace()()

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

這里,我給出了新版 Trace 函數(shù)的實(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ù)獲得當(dāng)前 Goroutine 的函數(shù)調(diào)用棧上的信息,runtime.Caller 的參數(shù)標(biāo)識(shí)的是要獲取的是哪一個(gè)棧幀的信息。當(dāng)參數(shù)為 0 時(shí),返回的是 Caller 函數(shù)的調(diào)用者的函數(shù)信息,在這里就是 Trace 函數(shù)。但我們需要的是 Trace 函數(shù)的調(diào)用者的信息,于是我們傳入 1。

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

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

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

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

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

三、增加 Goroutine 標(biāo)識(shí)

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

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

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

// $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 不是一個(gè)導(dǎo)出函數(shù),我們無法直接使用。我們可以把它復(fù)制出來改造一下:

// 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
}

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

好,接下來,我們?cè)?nbsp;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) }
}

從上面代碼看到,我們?cè)诔鋈肟谳敵龅母櫺畔⒅屑尤肓?nbsp;Goroutine ID 信息,我們輸出的 Goroutine ID 為 5 位數(shù)字,如果 ID 值不足 5 位,則左補(bǔ)零,這一切都是 Printf 函數(shù)的格式控制字符串“%05d”幫助我們實(shí)現(xiàn)的。這樣對(duì)齊 Goroutine ID 的位數(shù),為的是輸出信息格式的一致性更好。如果你的 Go 程序中 Goroutine 的數(shù)量超過了 5 位數(shù)可以表示的數(shù)值范圍,也可以自行調(diào)整控制字符串。

接下來,我們也要對(duì)示例進(jìn)行一些調(diào)整,將這個(gè)程序由單 Goroutine 改為多 Goroutine 并發(fā)的,這樣才能驗(yàn)證支持多 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()
}

新示例程序共有兩個(gè) Goroutine,main goroutine 的調(diào)用鏈為 A1 -> B1 -> C1 -> D,而另外一個(gè) Goroutine 的函數(shù)調(diào)用鏈為 A2 -> B2 -> C2 -> D。我們來看一下這個(gè)程序的執(zhí)行結(jié)果是否和原代碼中兩個(gè) 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 我們可以快速確認(rèn)某一行輸出是屬于哪個(gè) Goroutine 的。

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

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

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

對(duì)于程序員來說,縮進(jìn)是最能體現(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

那么我們就以這個(gè)形式為目標(biāo),考慮如何實(shí)現(xiàn)輸出這種帶縮進(jì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]    // 獲取當(dāng)前gid對(duì)應(yīng)的縮進(jìn)層次
    m[gid] = indents + 1 // 縮進(jìn)層次+1后存入map
    mu.Unlock()
    printTrace(gid, name, "->", indents+1)
    return func() {
        mu.Lock()
        indents := m[gid]    // 獲取當(dāng)前gid對(duì)應(yīng)的縮進(jìn)層次
        m[gid] = indents - 1 // 縮進(jìn)層次-1后存入map
        mu.Unlock()
        printTrace(gid, name, "<-", indents)
    }
}

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

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

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

運(yùn)行新版示例代碼,我們會(huì)得到下面的結(jié)果:

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

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

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

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

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

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

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

作為 trace 包的作者,我們有義務(wù)告訴大家如何使用 trace 包。在 Go 中,通常我們會(huì)用一個(gè) 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ù)表示一個(gè)示例,go test 命令會(huì)掃描 example_test.go 中的以 Example 為前綴的函數(shù)并執(zhí)行這些函數(shù)。

每個(gè) ExampleXXX 函數(shù)需要包含預(yù)期的輸出,就像上面 ExampleTrace 函數(shù)尾部那樣,我們?cè)谝淮蠖巫⑨屩刑峁┻@個(gè)函數(shù)執(zhí)行后的預(yù)期輸出,預(yù)期輸出的內(nèi)容從 // Output: 的下一行開始。go test 會(huì)將 ExampleTrace 的輸出與預(yù)期輸出對(duì)比,如果不一致,會(huì)報(bào)測(cè)試錯(cuò)誤。從這一點(diǎn),我們可以看出 example_test.go 也是 trace 包單元測(cè)試的一部分。

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

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

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

我們?cè)俑鶕?jù)之前介紹的帶有【可執(zhí)行文件的 Go 項(xiàng)目布局】,在 instrument_trace module 中增加 cmd/instrument 目錄,這個(gè)工具的 main 包就放在這個(gè)目錄下,而真正實(shí)現(xiàn)自動(dòng)注入 Trace 函數(shù)的代碼呢,被我們放在了 instrumenter 目錄下。

下面是變化后的 instrument_trace module 的目錄結(jié)構(gòu):

$tree ./instrument_trace -F
./instrument_trace
├── Makefile
├── cmd/
│   └── instrument/
│       └── main.go  # instrument命令行工具的main包
├── example_test.go
├── go.mod
├── go.sum
├── instrumenter/    # 自動(dòng)注入邏輯的相關(guān)結(jié)構(gòu)
│   ├── ast/
│   │   └── ast.go
│   └── instrumenter.go
└── trace.go

我們先來看一下 cmd/instrument/main.go 源碼,然后自上而下沿著 main 函數(shù)的調(diào)用邏輯逐一看一下這個(gè)功能的實(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 { // 對(duì)命令行參數(shù)個(gè)數(shù)進(jìn)行校驗(yàn)
        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" { // 對(duì)源文件擴(kuò)展名進(jìn)行校驗(yàn)
        usage()
        return
    }

    var ins instrumenter.Instrumenter // 聲明instrumenter.Instrumenter接口類型變量
    
    // 創(chuàng)建以ast方式實(shí)現(xiàn)Instrumenter接口的ast.instrumenter實(shí)例
    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 使用標(biāo)準(zhǔn)庫的 flag 包實(shí)現(xiàn)對(duì)命令行參數(shù)(這里是 -w)的解析,通過 os.Args 獲取待注入的 Go 源文件路徑。在完成對(duì)命令行參數(shù)個(gè)數(shù)與值的校驗(yàn)后,instrument 程序聲明了一個(gè) instrumenter.Instrumenter 接口類型變量 ins,然后創(chuàng)建了一個(gè)實(shí)現(xiàn)了 Instrumenter 接口類型的 ast.instrumenter 類型的實(shí)例,并賦值給變量 ins。

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

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

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

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

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

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

一旦我們通過 ast 相關(guān)包解析 Go 源碼得到相應(yīng)的抽象語法樹后,我們便可以操作這棵語法樹,并按我們的邏輯在語法樹中注入我們的 Trace 函數(shù),最后我們?cè)賹⑿薷暮蟮某橄笳Z法樹轉(zhuǎn)換為 Go 源碼,就完成了整個(gè)自動(dòng)注入的工作了。

了解了原理后,我們?cè)倏匆幌戮唧w的代碼實(shí)現(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) { // 如果整個(gè)源碼都不包含函數(shù)聲明,則無需注入操作,直接返回。
        return nil, nil
    }

    // 在AST上添加包導(dǎo)入語句
    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ù)對(duì)傳入的 Go 源文件中的源碼進(jìn)行解析,并得到對(duì)應(yīng)的抽象語法樹 AST,然后向 AST 中導(dǎo)入 Trace 函數(shù)所在的包,并向這個(gè) AST 的所有函數(shù)聲明注入 Trace 函數(shù)調(diào)用。

實(shí)際的注入操作發(fā)生在 instrumenter 的 addDeferTraceIntoFuncDecls 方法中,我們來看一下這個(gè)方法的實(shí)現(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ù)聲明,則注入跟蹤設(shè)施
            a.addDeferStmt(fd)
        }
    }
}

這個(gè)方法的邏輯十分清晰,就是遍歷語法樹上所有聲明語句,如果是函數(shù)聲明,就調(diào)用 instrumenter 的 addDeferStmt 方法進(jìn)行注入,如果不是,就直接返回。addDeferStmt 方法的實(shí)現(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語句,則要進(jìn)一步判斷是否是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()()",注入一個(gè)新的跟蹤語句
    // 在AST上構(gòu)造一個(gè)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 // 注入新構(gòu)造的defer語句
    fd.Body.List = newList
    return true
}

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

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

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

有了 instrument 工具后,我們?cè)賮砜纯慈绾问褂眠@個(gè)工具,在目標(biāo) Go 源文件中自動(dòng)注入跟蹤設(shè)施。

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

// instrument_trace/examples/demo/demo.go

package main

func foo() {
    bar()
}

func bar() {
}

func main() {
    foo()
}

我們首先構(gòu)建一下 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ù)自動(dòng)注入跟蹤設(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)榱讼旅孢@個(gè)樣子:

// 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()
}

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

由于 github.com/bigwhite/instrument_trace 并沒有真正上傳到 github.com 上,所以如果你要運(yùn)行 demo.go,我們可以為它配置一個(gè)下面這樣的 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 => ../../

這樣運(yùn)行 demo.go 就不會(huì)遇到障礙了:

$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

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

相關(guān)文章

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

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

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

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

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

    一文教你打造一個(gè)簡(jiǎn)易的Golang日志庫

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

    基于Golang編寫一個(gè)聊天工具

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

    windows安裝部署go超詳細(xì)實(shí)戰(zhàn)記錄(實(shí)測(cè)有用!)

    Golang語言在近年來因?yàn)槠涓咝阅堋⒕幾g速度快、開發(fā)成本低等特點(diǎn)逐漸得到大家的青睞,這篇文章主要給大家介紹了關(guān)于windows安裝部署go超詳細(xì)實(shí)戰(zhàn)的相關(guān)資料,需要的朋友可以參考下
    2023-02-02
  • Golang Cron 定時(shí)任務(wù)的實(shí)現(xiàn)示例

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

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

    Golang中指針的使用詳解

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

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

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

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

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

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

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

最新評(píng)論