本文 AI 產出,尚未審核

Golang - 測試與除錯

主題:除錯工具(Delve, GDB)


簡介

在開發 Go 程式時,除錯是找出錯誤、提升程式品質的關鍵步驟。即使語法簡潔、編譯快速,實務專案仍會因為邏輯錯誤、競爭條件或第三方套件的行為不符預期而卡關。若只能靠 fmt.Println 追蹤變數值,往往會讓程式碼雜亂且難以維護。

Go 官方提供了兩套成熟的除錯器:Delve(專為 Go 設計)與 GDB(通用的原生除錯器)。掌握這兩個工具,不僅能在本機快速定位問題,還能在容器、遠端或 CI/CD 流程中進行自動化除錯。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步建立除錯的思維模型。


核心概念

1. Delve 基礎

Delve(dlv)是 Go 社群最常使用的除錯器,支援 斷點、單步執行、檢視變數、堆疊追蹤 等功能。它直接操作 Go 的 runtime,能正確顯示 goroutine、channel、map 等語言特性。

安裝與簡易啟動

# 以 Homebrew 為例(macOS / Linux)
brew install delve

# 直接在程式目錄啟動除錯
dlv debug ./cmd/app

Tip:若專案使用 go.mod,建議在專案根目錄執行 dlv debug,Delve 會自動載入相依套件。

2. GDB 與 Go

GDB 是 Linux/Unix 系統的通用除錯器,透過 DWARF 調試資訊也能除錯 Go 程式。雖然 GDB 不支援 Go 的高階抽象(如 goroutine 名稱),但在 Cgo裸機混合語言 專案中仍相當有用。

以 GDB 除錯 Go 程式

# 先編譯帶除錯資訊的執行檔
go build -gcflags "all=-N -l" -o myapp main.go

# 使用 GDB 附加
gdb ./myapp

注意-N -l 參數會關閉最佳化與內聯,確保除錯資訊完整。

3. 斷點與條件斷點

  • 普通斷點:在指定行號或函式入口暫停程式。
  • 條件斷點:僅在變數滿足特定條件時才停下,適合排查「偶發」錯誤。

Delve 設定條件斷點

// main.go
package main

import "fmt"

func compute(n int) int {
    if n%2 == 0 {
        return n * 2
    }
    return n * 3
}

func main() {
    for i := 1; i <= 10; i++ {
        fmt.Println(i, compute(i))
    }
}
dlv debug
(dlv) break main.compute
(dlv) condition main.compute n == 5   # 只在 n 為 5 時停下
(dlv) continue

4. 檢視 Goroutine 與 Channel

Delve 能列出所有 goroutine,並顯示每個 goroutine 的堆疊與狀態。這在 競爭條件死鎖 的排查上非常有幫助。

// worker.go
package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan<- int) {
    fmt.Printf("worker %d start\n", id)
    time.Sleep(time.Duration(id) * time.Second)
    ch <- id * 10
    fmt.Printf("worker %d done\n", id)
}

func main() {
    ch := make(chan int)
    for i := 1; i <= 3; i++ {
        go worker(i, ch)
    }
    for i := 1; i <= 3; i++ {
        fmt.Println("result:", <-ch)
    }
}
dlv debug
(dlv) continue          # 程式跑到阻塞點
(dlv) goroutine list    # 顯示所有 goroutine
(dlv) goroutine 2 stack # 查看第 2 個 goroutine 的呼叫堆疊

5. 使用 GDB 檢查 Cgo 呼叫

當 Go 程式與 C 程式庫交互時,GDB 能直接觀察 C 層的記憶體與暫存器。

/* hello.c */
#include <stdio.h>

void Hello(const char* s) {
    printf("C says: %s\n", s);
}
// main.go
package main

/*
#cgo CFLAGS: -g -O0
#cgo LDFLAGS: -L. -lhello
#include "hello.h"
*/
import "C"

func main() {
    C.Hello(C.CString("Hello from Go"))
}
go build -gcflags "all=-N -l" -o hello
gdb ./hello
(gdb) break Hello
(gdb) run

常見陷阱與最佳實踐

陷阱 說明 解法/最佳實踐
除錯資訊被最佳化移除 使用 go build 的預設最佳化會刪除變數、行號,導致斷點無法對應。 加上 -gcflags "all=-N -l",或在 go test -run TestX -v -cover 時加入同樣旗標。
Goroutine ID 不易辨識 GDB 只能看到原始執行緒,無法直接映射到 Go 的 goroutine。 使用 Delve 的 goroutine list,或在程式碼中手動打印 runtime.Goid()(需自行實作)作為輔助。
遠端容器除錯失敗 容器內缺少 dlvgdb 或除錯資訊。 在 Dockerfile 中加入 apk add --no-cache delve(Alpine)或 apt-get install -y gdb,並使用 -gcflags 重新編譯。
斷點位置不正確 行號變動或檔案路徑不同,斷點設定失效。 使用相對路徑或在 Delve 中使用 break <package>.<func> 方式設定斷點。
Channel 死鎖難以追蹤 只看程式碼不易判斷哪裡卡住。 在 Delive 中使用 goroutine list + goroutine <id> stack,或在程式中加入 select { default: } 暫時釋放阻塞,觀察行為。

最佳實踐

  1. 先寫單元測試:測試失敗時再使用除錯器,可縮小範圍。
  2. 保持除錯資訊完整:開發階段統一使用 -gcflags "all=-N -l",避免因最佳化隱藏錯誤。
  3. 善用條件斷點:對於迴圈或大量資料,條件斷點能大幅提升除錯效率。
  4. 在 CI 中加入除錯腳本:若測試在 CI 失敗,可自動產生 core dump,利用 GDB 分析。
  5. 紀錄除錯步驟:將斷點、變數檢查的指令寫入 README 或 issue,方便團隊成員複現。

實際應用場景

場景 使用工具 為什麼選擇此工具
本機開發 Delve 直接支援 goroutine、channel,操作簡潔。
Cgo 整合 GDB 必須檢視 C 程式碼與記憶體,GDB 提供完整的 C 語言除錯功能。
容器化部署 Delve (remote) 可透過 dlv connect <host>:port 連線容器內的除錯服務。
大型分散式服務 GDB + core dump 在服務崩潰後產生 core,使用 GDB 分析堆疊,快速定位 panic 原因。
競爭條件排查 Delve 的 goroutine list + race detector 先用 go run -race 捕捉競爭,再用 Delve 觀察卡住的 goroutine。

總結

  • Delve 是 Go 開發者的首選除錯器,支援斷點、條件斷點、goroutine 觀察與 channel 追蹤,操作上與 IDE(VSCode、GoLand)高度整合。
  • GDB 雖非專屬 Go,但在 Cgocore dump跨語言 場景中仍不可或缺。
  • 透過 正確的編譯旗標條件斷點goroutine 列表,可以大幅提升除錯效率,避免因最佳化或資訊缺失而陷入「看不見的錯誤」。
  • 最後,除錯不只是找錯,更是一種程式思考的方式:先寫測試、保持程式碼可觀測、在必要時使用工具快速定位,才能在實務專案中維持高品質與快速交付。

祝你在 Go 的除錯旅程中,以最少的時間找出最多的問題,寫出更穩定、更易維護的程式!