14 Go Debug

一、Debug概述

在程序開發(fā)過程中,多多少少會(huì)出現(xiàn)編譯出錯(cuò),運(yùn)行時(shí)錯(cuò)誤等等,有些是語法錯(cuò)誤,有些是拼寫錯(cuò)誤,有些是語言特性沒理解到位,這些因素都會(huì)造成程序開發(fā)的暫時(shí)中斷,影響開發(fā)進(jìn)度。所以常見的錯(cuò)誤還是要認(rèn)識(shí)并避免的,這里有一篇國(guó)外Gopher整理的《Go 陷阱及常見錯(cuò)誤》,可以讓你快速定位常見的問題,縮短debug的時(shí)間。但現(xiàn)實(shí)世界并非一帆風(fēng)順,有時(shí)讓你走運(yùn)遇到一些并不常見,或者不容易發(fā)現(xiàn)的錯(cuò)誤時(shí),你需要一些調(diào)試工具來幫你排查錯(cuò)誤,以下就來介紹Go的調(diào)試工具。

二、Go 調(diào)試工具

目前Go語言支持GDB、LLDB和Delve幾種調(diào)試器。其中GDB是最早支持的調(diào)試工具,LLDB是macOS系統(tǒng)推薦的標(biāo)準(zhǔn)調(diào)試工具。但是GDB和LLDB對(duì)Go語言的專有特性都缺乏很大支持,而只有Delve是專門為Go語言設(shè)計(jì)開發(fā)的調(diào)試工具。而且Delve本身也是采用Go語言開發(fā),對(duì)Windows平臺(tái)也提供了一樣的支持。本節(jié)我們基于Delve簡(jiǎn)單解釋如何調(diào)試Go匯編程序。

Delve

Delve是Go程序的源代碼級(jí)調(diào)試器,它使你能夠通過控制流程的執(zhí)行來與你的程序進(jìn)行交互,評(píng)估變量,并提供線程/goroutine狀態(tài),CPU寄存器狀態(tài)等信息。此工具的目標(biāo)是為調(diào)試Go程序提供簡(jiǎn)單而強(qiáng)大的界面。

使用--將標(biāo)志傳遞給正在調(diào)試的程序,例如:

dlv exec ./hello -- server --config conf/config.toml

1.啟動(dòng)選項(xiàng)
  --accept-multiclient: 允許headless服務(wù)器接受多個(gè)客戶端連接。請(qǐng)注意,服務(wù)器API不可重入,客戶端必須進(jìn)行協(xié)調(diào)。
  --api-version int :headless時(shí)選擇API版本。(默認(rèn)1)
  --backend string  :后端選擇,可能的值為:
            default :  在macOS上使用lldb,在其他地方使用native。
            native :   原生的后端。
            lldb:      使用lldb-server或debugserver。
            rr :       使用mozilla rr(https://github.com/mozilla/rr)。
    (默認(rèn)為“default”)
  --build-flags string:構(gòu)建標(biāo)志,傳遞給編譯器。
  --check-go-version:檢查使用的Go版本是否與Delve兼容。(默認(rèn)為true)
  --headless:僅在headless模式下運(yùn)行調(diào)試服務(wù)器。
  --init string :Init文件,由終端客戶端執(zhí)行。
  -l,--listen string :調(diào)試服務(wù)器監(jiān)聽地址。(默認(rèn)為“l(fā)ocalhost:0”)
  --log:啟用調(diào)試服務(wù)器日志記錄。
  --log-dest string :將日志寫入指定的文件或文件描述符。如果參數(shù)是數(shù)字,則它將被解釋為文件描述符,否則將被解釋為文件路徑。此選項(xiàng)還將在headless模式下重定向“API偵聽”消息。
  --log-output string:以逗號(hào)分隔的組件列表,它們應(yīng)該產(chǎn)生調(diào)試輸出,可能的值:
            debugger:      日志調(diào)試器命令
            gdbwire:       與gdbserial后端的日志連接
            lldbout:       將debugserver / lldb的輸出復(fù)制到標(biāo)準(zhǔn)輸出
            debuglineerr:  記錄可恢復(fù)的錯(cuò)誤,讀取.debug_line
            rpc:           記錄所有RPC消息
            fncall:        日志函數(shù)調(diào)用協(xié)議
            minidump:      記錄minidump加載
            使用--log啟用日志記錄時(shí),默認(rèn)為“debugger”。
  --wd string:     運(yùn)行程序的工作目錄。(默認(rèn)“.”)
也可以看看dlv下的各個(gè)工具包,幫助你進(jìn)行不同要求的調(diào)試:
  • [dlv attach](dlv_attach.md) - 附加到正在運(yùn)行的進(jìn)程并開始調(diào)試。
  • [dlv connect](dlv_connect.md) - 連接到服務(wù)器調(diào)試。
  • [dlv core](dlv_core.md) - 檢查核心轉(zhuǎn)儲(chǔ)。
  • [dlv debug](dlv_debug.md) - 編譯并開始調(diào)試當(dāng)前目錄或指定包中的主包。
  • [dlv exec](dlv_exec.md) - 執(zhí)行預(yù)編譯二進(jìn)制文件,并開始調(diào)試會(huì)話。
  • [dlv replay](dlv_replay.md) - 重播rr跟蹤。
  • [dlv run](dlv_run.md) - 不推薦使用的命令。請(qǐng)改用“debug”。
  • [dlv test](dlv_test.md) - 編譯測(cè)試二進(jìn)制文件并開始調(diào)試程序。
  • [dlv trace](dlv_trace.md) - 編譯并開始跟蹤程序。
  • [dlv version](dlv_version.md) - 打印版本。

其內(nèi)部命令和選項(xiàng)可查看文檔,這里不再列出:
Github delve 文檔

簡(jiǎn)單入門

首先根據(jù)官方的文檔正確安裝Delve調(diào)試器。我們會(huì)先構(gòu)造一個(gè)簡(jiǎn)單的Go語言代碼,用于熟悉下Delve的簡(jiǎn)單用法。

創(chuàng)建main.go文件,main函數(shù)先通過循初始化一個(gè)切片,然后輸出切片的內(nèi)容:

package main

import (
    "fmt"
)

func main() {
    nums := make([]int, 5)
    for i := 0; i < len(nums); i++ {
        nums[i] = i * i
    }
    fmt.Println(nums)
}

下面我們用dlv debug工具簡(jiǎn)單使用一下,命令行進(jìn)入包所在目錄,然后輸入dlv debug命令進(jìn)入調(diào)試:

$ dlv debug
Type 'help' for list of commands.
(dlv)

輸入help命令可以查看到Delve提供的調(diào)試命令列表:

(dlv) help
The following commands are available:
    args ------------------------ Print function arguments.
    break (alias: b) ------------ Sets a breakpoint.
    breakpoints (alias: bp) ----- Print out info for active breakpoints.
    clear ----------------------- Deletes breakpoint.
    clearall -------------------- Deletes multiple breakpoints.
    condition (alias: cond) ----- Set breakpoint condition.
    config ---------------------- Changes configuration parameters.
    continue (alias: c) --------- Run until breakpoint or program termination.
    disassemble (alias: disass) - Disassembler.
    down ------------------------ Move the current frame down.
    exit (alias: quit | q) ------ Exit the debugger.
    frame ----------------------- Set the current frame, or execute command...
    funcs ----------------------- Print list of functions.
    goroutine ------------------- Shows or changes current goroutine
    goroutines ------------------ List program goroutines.
    help (alias: h) ------------- Prints the help message.
    list (alias: ls | l) -------- Show source code.
    locals ---------------------- Print local variables.
    next (alias: n) ------------- Step over to next source line.
    on -------------------------- Executes a command when a breakpoint is hit.
    print (alias: p) ------------ Evaluate an expression.
    regs ------------------------ Print contents of CPU registers.
    restart (alias: r) ---------- Restart process.
    set ------------------------- Changes the value of a variable.
    source ---------------------- Executes a file containing a list of delve...
    sources --------------------- Print list of source files.
    stack (alias: bt) ----------- Print stack trace.
    step (alias: s) ------------- Single step through program.
    step-instruction (alias: si)  Single step a single cpu instruction.
    stepout --------------------- Step out of the current function.
    thread (alias: tr) ---------- Switch to the specified thread.
    threads --------------------- Print out info for every traced thread.
    trace (alias: t) ------------ Set tracepoint.
    types ----------------------- Print list of types
    up -------------------------- Move the current frame up.
    vars ------------------------ Print package variables.
    whatis ---------------------- Prints type of an expression.
Type help followed by a command for full documentation.
(dlv)

每個(gè)Go程序的入口是main.main函數(shù),我們可以用break在此設(shè)置一個(gè)斷點(diǎn):

(dlv) break main.main
Breakpoint 1 set at 0x10ae9b8 for main.main() ./main.go:7

然后通過breakpoints查看已經(jīng)設(shè)置的所有斷點(diǎn):

(dlv) breakpoints
Breakpoint unrecovered-panic at 0x102a380 for runtime.startpanic()
    /usr/local/go/src/runtime/panic.go:588 (0)
        print runtime.curg._panic.arg
Breakpoint 1 at 0x10ae9b8 for main.main() ./main.go:7 (0)

我們發(fā)現(xiàn)除了我們自己設(shè)置的main.main函數(shù)斷點(diǎn)外,Delve內(nèi)部已經(jīng)為panic異常函數(shù)設(shè)置了一個(gè)斷點(diǎn)。

通過vars命令可以查看全部包級(jí)的變量。因?yàn)樽罱K的目標(biāo)程序可能含有大量的全局變量,我們可以通過一個(gè)正則參數(shù)選擇想查看的全局變量:

(dlv) vars main
main.initdone· = 2
runtime.main_init_done = chan bool 0/0
runtime.mainStarted = true
(dlv)

然后就可以通過continue命令讓程序運(yùn)行到下一個(gè)斷點(diǎn)處:

(dlv) continue
> main.main() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x10ae9b8)
     2:
     3: import (
     4:         "fmt"
     5: )
     6:
=>   7: func main() {
     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
(dlv)

輸入next命令單步執(zhí)行進(jìn)入main函數(shù)內(nèi)部:

(dlv) next
> main.main() ./main.go:8 (PC: 0x10ae9cf)
     3: import (
     4:         "fmt"
     5: )
     6:
     7: func main() {
=>   8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv)

進(jìn)入函數(shù)之后可以通過args和locals命令查看函數(shù)的參數(shù)和局部變量:

(dlv) args
(no args)
(dlv) locals
nums = []int len: 842350763880, cap: 17491881, nil

因?yàn)閙ain函數(shù)沒有參數(shù),因此args命令沒有任何輸出。而locals命令則輸出了局部變量nums切片的值:此時(shí)切片還未完成初始化,切片的底層指針為nil,長(zhǎng)度和容量都是一個(gè)隨機(jī)數(shù)值。

再次輸入next命令單步執(zhí)行后就可以查看到nums切片初始化之后的結(jié)果了:

(dlv) next
> main.main() ./main.go:9 (PC: 0x10aea12)
     4:         "fmt"
     5: )
     6:
     7: func main() {
     8:         nums := make([]int, 5)
=>   9:         for i := 0; i < len(nums); i++ {
    10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv) locals
nums = []int len: 5, cap: 5, [...]
i = 17601536
(dlv)

此時(shí)因?yàn)檎{(diào)試器已經(jīng)到了for語句行,因此局部變量出現(xiàn)了還未初始化的循環(huán)迭代變量i。

下面我們通過組合使用break和condition命令,在循環(huán)內(nèi)部設(shè)置一個(gè)條件斷點(diǎn),當(dāng)循環(huán)變量i等于3時(shí)斷點(diǎn)生效:

(dlv) break main.go:10
Breakpoint 2 set at 0x10aea33 for main.main() ./main.go:10
(dlv) condition 2 i==3
(dlv)

然后通過continue執(zhí)行到剛設(shè)置的條件斷點(diǎn),并且輸出局部變量:

(dlv) continue
> main.main() ./main.go:10 (hits goroutine(1):1 total:1) (PC: 0x10aea33)
     5: )
     6:
     7: func main() {
     8:         nums := make([]int, 5)
     9:         for i := 0; i < len(nums); i++ {
=>  10:                 nums[i] = i * i
    11:         }
    12:         fmt.Println(nums)
    13: }
(dlv) locals
nums = []int len: 5, cap: 5, [...]
i = 3
(dlv) print nums
[]int len: 5, cap: 5, [0,1,4,0,0]
(dlv)

我們發(fā)現(xiàn)當(dāng)循環(huán)變量i等于3時(shí),nums切片的前3個(gè)元素已經(jīng)正確初始化。

我們還可以通過stack查看當(dāng)前執(zhí)行函數(shù)的棧幀信息:

(dlv) stack
0  0x00000000010aea33 in main.main
   at ./main.go:10
1  0x000000000102bd60 in runtime.main
   at /usr/local/go/src/runtime/proc.go:198
2  0x0000000001053bd1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:2361
(dlv)

或者通過goroutine和goroutines命令查看當(dāng)前Goroutine相關(guān)的信息:

(dlv) goroutine
Thread 101686 at ./main.go:10
Goroutine 1:
  Runtime: ./main.go:10 main.main (0x10aea33)
  User: ./main.go:10 main.main (0x10aea33)
  Go: /usr/local/go/src/runtime/asm_amd64.s:258 runtime.rt0_go (0x1051643)
  Start: /usr/local/go/src/runtime/proc.go:109 runtime.main (0x102bb90)
(dlv) goroutines
[4 goroutines]
* Goroutine 1 - User: ./main.go:10 main.main (0x10aea33) (thread 101686)
  Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
  Goroutine 3 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
  Goroutine 4 - User: /usr/local/go/src/runtime/proc.go:292 \
                runtime.gopark (0x102c189)
(dlv)

最后完成調(diào)試工作后輸入quit命令退出調(diào)試器。至此我們已經(jīng)掌握了Delve調(diào)試器器的簡(jiǎn)單用法。

總之,遇到bug時(shí)別慌張,當(dāng)掌握一些常見錯(cuò)誤和一些的調(diào)試方法后,你也可以科學(xué)的定位問題,高效的排查錯(cuò)誤,讓開發(fā)效率顯著提升。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容