Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go 官方工具與 Runtime 診斷總整理

基準版本:go1.21.6 linux/amd64

範圍:

  • 官方 go 指令
  • 官方 go tool 子工具
  • 官方 runtime 診斷/觀測機制
  • GODEBUG 與相關環境變數

不含:

  • 第三方工具,例如 dlvstaticcheckgolangci-lint

先講結論

如果你只想知道「什麼時候用什麼」:

情境先用什麼
程式變慢、CPU 飆高go test -cpuprofile + go tool pprof
記憶體暴增、疑似洩漏go test -memprofile + go tool pprof
Goroutine 卡住、鎖競爭、阻塞block profilemutex profilegoroutine profilego tool trace
想看排程器、syscall、GC、阻塞全貌go test -trace + go tool trace
懷疑資料競態go test -race
想知道測試覆蓋率go test -cover / -coverprofile / go tool cover
想看 runtime GC 行為GODEBUG=gctrace=1
想看 scheduler 行為GODEBUG=schedtrace=1000,scheddetail=1
想調 GC 壓力GOGCGOMEMLIMIT
想看 panic 時所有 goroutineGOTRACEBACK=allsystem
只想快速看匯編、符號、物件內容go tool objdumpnmaddr2line
想抓 API 文件或符號說明go doc / go tool doc
想做程式碼檢查go vet
想批次修正舊 APIgo fix

範例程式碼(所有範例均已驗證可執行)

以下範例以這個模組為基礎:

mkdir demo && cd demo
go mod init demo

範例主程式 main.go

package main

import (
    "fmt"
    "math"
    "runtime"
    "runtime/debug"
    "sync"
    "time"
)

func isPrime(n int) bool {
    if n < 2 {
        return false
    }
    for i := 2; i <= int(math.Sqrt(float64(n))); i++ {
        if n%i == 0 {
            return false
        }
    }
    return true
}

func countPrimes(max int) int {
    count := 0
    for i := 2; i <= max; i++ {
        if isPrime(i) {
            count++
        }
    }
    return count
}

func allocStrings(n int) []string {
    result := make([]string, n)
    for i := range result {
        result[i] = fmt.Sprintf("item-%d", i)
    }
    return result
}

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    fmt.Printf("Go version: %s\n", runtime.Version())
    fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))

    count := countPrimes(100000)
    fmt.Printf("Primes below 100000: %d\n", count)

    var ms runtime.MemStats
    runtime.ReadMemStats(&ms)
    fmt.Printf("HeapAlloc: %d KB\n", ms.HeapAlloc/1024)

    if info, ok := debug.ReadBuildInfo(); ok {
        fmt.Printf("Module: %s\n", info.Main.Path)
    }
    _ = time.Now()
}

範例測試 main_test.go(完整示範各種 profiling)

package main

import (
    "fmt"
    "os"
    "runtime"
    "runtime/debug"
    "runtime/pprof"
    "runtime/trace"
    "sync"
    "testing"
    "time"
)

// ===== 基本功能測試 =====

func TestIsPrime(t *testing.T) {
    cases := []struct {
        n    int
        want bool
    }{
        {1, false}, {2, true}, {3, true}, {4, false},
        {17, true}, {100, false}, {97, true},
    }
    for _, c := range cases {
        if got := isPrime(c.n); got != c.want {
            t.Errorf("isPrime(%d) = %v, want %v", c.n, got, c.want)
        }
    }
}

func TestCountPrimes(t *testing.T) {
    if got := countPrimes(100); got != 25 { // π(100) = 25
        t.Errorf("countPrimes(100) = %d, want 25", got)
    }
}

func TestCounter(t *testing.T) {
    c := &Counter{}
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Inc()
        }()
    }
    wg.Wait()
    if got := c.Get(); got != 100 {
        t.Errorf("Counter = %d, want 100", got)
    }
}

// ===== Benchmark =====

func BenchmarkIsPrime(b *testing.B) {
    for i := 0; i < b.N; i++ {
        isPrime(999983)
    }
}

func BenchmarkCountPrimes(b *testing.B) {
    for i := 0; i < b.N; i++ {
        countPrimes(10000)
    }
}

func BenchmarkAllocStrings(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = allocStrings(1000)
    }
}

func BenchmarkCounterParallel(b *testing.B) {
    c := &Counter{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

// 預分配 vs 動態增長對比
var sink []string

func BenchmarkAllocWithGrow(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        var s []string
        for j := 0; j < 100; j++ {
            s = append(s, fmt.Sprintf("x%d", j))
        }
        sink = s
    }
}

func BenchmarkAllocWithPrealloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := make([]string, 0, 100)
        for j := 0; j < 100; j++ {
            s = append(s, fmt.Sprintf("x%d", j))
        }
        sink = s
    }
}

// ===== CPU Profile(程式內手動) =====

func TestCPUProfile(t *testing.T) {
    f, _ := os.Create("cpu.out")
    defer f.Close()
    if err := pprof.StartCPUProfile(f); err != nil {
        t.Logf("skip: %v", err) // 外部已開 -cpuprofile 時會衝突
        return
    }
    defer pprof.StopCPUProfile()
    t.Logf("Primes: %d (cpu.out written)", countPrimes(50000))
}

// ===== Memory Profile =====

func TestMemProfile(t *testing.T) {
    for i := 0; i < 5; i++ {
        _ = allocStrings(10000)
    }
    runtime.GC()
    f, _ := os.Create("mem.out")
    defer f.Close()
    pprof.WriteHeapProfile(f)
    t.Log("mem.out written")
}

// ===== Block Profile =====

func TestBlockProfile(t *testing.T) {
    runtime.SetBlockProfileRate(1)
    defer runtime.SetBlockProfileRate(0)

    ch := make(chan int)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() { defer wg.Done(); ch <- 1 }()
    }
    go func() {
        for i := 0; i < 10; i++ { <-ch; time.Sleep(time.Millisecond) }
    }()
    wg.Wait()

    f, _ := os.Create("block.out")
    defer f.Close()
    pprof.Lookup("block").WriteTo(f, 0)
    t.Log("block.out written")
}

// ===== Mutex Profile =====

func TestMutexProfile(t *testing.T) {
    runtime.SetMutexProfileFraction(1)
    defer runtime.SetMutexProfileFraction(0)

    c := &Counter{}
    var wg sync.WaitGroup
    for i := 0; i < 50; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 100; j++ { c.Inc() }
        }()
    }
    wg.Wait()

    f, _ := os.Create("mutex.out")
    defer f.Close()
    pprof.Lookup("mutex").WriteTo(f, 0)
    t.Logf("mutex.out written, counter=%d", c.Get())
}

// ===== Trace =====

func TestTrace(t *testing.T) {
    f, _ := os.Create("trace.out")
    defer f.Close()
    if err := trace.Start(f); err != nil {
        t.Logf("skip: %v", err) // 外部已開 -trace 時會衝突
        return
    }
    defer trace.Stop()

    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() { defer wg.Done(); _ = countPrimes(5000) }()
    }
    wg.Wait()
    t.Log("trace.out written")
}

// ===== Goroutine Profile =====

func safeGoroutine(done <-chan struct{}) {
    for {
        select {
        case <-done:
            return
        case <-time.After(10 * time.Millisecond):
        }
    }
}

func TestGoroutineProfile(t *testing.T) {
    done := make(chan struct{})
    for i := 0; i < 3; i++ {
        go safeGoroutine(done)
    }
    time.Sleep(10 * time.Millisecond)

    f, _ := os.Create("goroutine.out")
    defer f.Close()
    pprof.Lookup("goroutine").WriteTo(f, 1)
    close(done)
    t.Logf("goroutine.out written, goroutines=%d", runtime.NumGoroutine())
}

// ===== MemStats =====

func TestMemStats(t *testing.T) {
    var before, after runtime.MemStats
    runtime.ReadMemStats(&before)
    _ = allocStrings(100000)
    runtime.GC()
    runtime.ReadMemStats(&after)
    t.Logf("HeapAlloc before: %d KB", before.HeapAlloc/1024)
    t.Logf("HeapAlloc after:  %d KB", after.HeapAlloc/1024)
    t.Logf("NumGC: %d", after.NumGC)
    t.Logf("TotalAlloc: %d KB", after.TotalAlloc/1024)
}

// ===== Fuzz =====

func FuzzIsPrime(f *testing.F) {
    f.Add(0); f.Add(1); f.Add(2); f.Add(97); f.Add(-1)
    f.Fuzz(func(t *testing.T, n int) {
        _ = isPrime(n) // 只驗不 panic
    })
}

// ===== Escape Analysis =====

func newOnHeap() *int { x := 42; return &x } // → heap
func stayOnStack() int { x := 42; return x } // → stack

func TestEscapeAnalysis(t *testing.T) {
    p := newOnHeap()
    t.Logf("heap ptr: %p = %d", p, *p)
    t.Logf("stack val: %d", stayOnStack())
}

// ===== runtime/debug =====

func TestRuntimeDebug(t *testing.T) {
    old := debug.SetGCPercent(50)
    defer debug.SetGCPercent(old)
    t.Logf("old GC percent: %d", old)

    debug.FreeOSMemory()

    if info, ok := debug.ReadBuildInfo(); ok {
        t.Logf("module: %s", info.Main.Path)
    }

    var stats debug.GCStats
    debug.ReadGCStats(&stats)
    t.Logf("NumGC: %d", stats.NumGC)
}

// ===== 列出所有可用 profile =====

func TestListProfiles(t *testing.T) {
    for _, p := range pprof.Profiles() {
        t.Logf("  %s (count=%d)", p.Name(), p.Count())
    }
}

1. 高階 go 指令

這些是平常最常用的入口。

指令用途什麼時候用
go build編譯 package / binary驗證是否可編譯、產出執行檔、加建置旗標
go run臨時編譯並執行小工具、範例、一次性腳本
go test跑測試/benchmark/fuzz平時測試、效能分析、race 檢查、coverage
go vet找高機率 bugCI、提交前靜態檢查
go fmt格式化提交前、批次整理
go fix自動遷移舊 API 用法升 Go 版本後清舊語法
go doc查 package / symbol 文件忘記 API、查標準庫
go env看 Go 環境排查環境、CGO、module、交叉編譯問題
go list列 package/module 資訊腳本、自動化、查依賴
go generate執行 //go:generatecodegen、mock、stringer 類流程
go install安裝 binary裝 CLI 或編譯主程式到 $GOBIN
go get加/升依賴更新 module 依賴
go modmodule 維護tidyvendorgraphwhy
go workworkspace 維護多 module 聯合開發
go version看工具鏈版本排查版本差異
go tool跑低階工具看匯編、trace、pprof、cover 等
go bug產生 issue 範本要回報 Go 問題時

go build 常見診斷旗標

旗標用途什麼時候用
-race資料競態偵測共享資料疑似沒鎖好
-cover編譯時插 coverage instrumentation要跑可執行檔 coverage
-asanAddressSanitizer 互通cgo / native memory 問題
-msanMemorySanitizer 互通cgo / 未初始化記憶體問題
-pgoProfile-guided optimization熱點穩定、要擠壓效能
-gcflags=all=-m看 escape analysis / inline 決策查 allocation、多餘 heap escape
-gcflags=all=-S看組合語言輸出極限效能分析
-ldflagslink 階段參數瘦身、注入版本字串
-trimpath去除本機絕對路徑可重現建置、乾淨 binary
-work保留暫存建置目錄排查編譯流程
-x顯示實際執行命令排查 toolchain 行為

Escape Analysis 範例

go build -gcflags='-m' ./...

輸出節錄(實測):

./main.go:13:6: can inline isPrime
./main.go:25:6: can inline countPrimes
./main.go:37:16: make([]string, n) escapes to heap
./main.go:39:38: i escapes to heap

解讀:

  • can inline:函式夠小,會被 inline,無呼叫開銷
  • escapes to heap:物件分配在 heap,GC 需管理
  • stack 上的變數不會出現在這裡

2. go test 其實是最重要的分析入口

大多數官方分析流程都從 go test 開始。

功能型旗標

旗標用途什麼時候用
-run只跑特定測試縮小問題範圍
-bench跑 benchmark效能基準
-benchmem顯示 allocation 次數與大小盯 allocation regression
-count重跑多次確認 flaky 或 benchmark 穩定性
-cpu多組 GOMAXPROCS看並行縮放性
-parallel控制測試平行度降噪、避免資源搶占
-shuffle打亂順序揪出順序相依測試
-fuzz啟動 fuzzing不信任輸入、parser、decoder、protocol
-jsonJSON 格式輸出丟 CI、分析工具
-timeout設定逾時防止測試掛死
-v詳細輸出看每個 test 的 log
-short跳過耗時測試快速 smoke test

分析型旗標

旗標輸出什麼時候用
-cpuprofile=cpu.outCPU profileCPU 熱點分析
-memprofile=mem.outMemory profileallocation / heap 分析
-blockprofile=block.out阻塞 profilechannel / mutex / syscall 卡住
-mutexprofile=mutex.outmutex contention profile鎖競爭
-trace=trace.outexecution trace排程、GC、阻塞、syscall 全景
-coverprofile=cover.outcoverage profile覆蓋率報告
-blockprofilerate=1開啟完整阻塞取樣要抓 block 時常一起開
-mutexprofilefraction=1提高 mutex 取樣鎖競爭很重時
-memprofilerate=1所有 allocation 都採樣要極精準記憶體分析時,代價高

典型命令(均已驗證)

# 所有測試加 race detector
go test ./... -race

# 跑指定 benchmark,印 allocation 統計
go test ./... -bench=BenchmarkCountPrimes -benchmem
# BenchmarkCountPrimes-16    4728    239650 ns/op    0 B/op    0 allocs/op

# 只跑特定測試,產生 CPU profile
go test ./... -bench=BenchmarkCountPrimes -cpuprofile=cpu.out

# 只跑特定測試,產生 memory profile
go test ./... -bench=BenchmarkAllocStrings -memprofile=mem.out

# block profile(需先在程式內設 rate 或加 -blockprofilerate)
go test ./... -run TestBlockProfile -blockprofile=block.out -blockprofilerate=1

# mutex profile
go test ./... -run TestMutexProfile -mutexprofile=mutex.out -mutexprofilefraction=1

# trace
go test ./... -run TestTrace -trace=trace.out

# coverage
go test ./... -coverprofile=cover.out

# 打亂測試順序(找隱性依賴)
go test ./... -shuffle=on -count=3

Benchmark 結果說明

BenchmarkIsPrime-16        816644    1500 ns/op    0 B/op    0 allocs/op
                   ^          ^         ^            ^           ^
              GOMAXPROCS   iteration  每次耗時     每次分配    每次分配次數

預分配 vs 動態增長(實測結果)

go test -bench='BenchmarkAllocWith' -benchmem
BenchmarkAllocWithGrow-16       232879    5044 ns/op    4391 B/op    108 allocs/op
BenchmarkAllocWithPrealloc-16   244104    4445 ns/op    2102 B/op    101 allocs/op

結論:預分配容量可減少約一半 allocation 次數和記憶體用量。


3. PProf 全家桶

這組工具專門處理「哪裡花最多資源」。

runtime/pprof

用途:

  • 在程式內手動產生 profile
  • 適合 CLI、batch job、非 HTTP 程式

什麼時候用:

  • 你的程式不是長駐 HTTP 服務
  • 想精準控制開始/結束分析時間點

常用 API:

import "runtime/pprof"

// CPU
f, _ := os.Create("cpu.out")
pprof.StartCPUProfile(f)
// ... 你的工作 ...
pprof.StopCPUProfile()
f.Close()

// Heap(任意時刻抓快照)
f, _ := os.Create("mem.out")
runtime.GC()
pprof.WriteHeapProfile(f)
f.Close()

// 任意具名 profile
f, _ := os.Create("block.out")
pprof.Lookup("block").WriteTo(f, 0)
f.Close()

// 所有可用 profile 名稱
for _, p := range pprof.Profiles() {
    fmt.Printf("%s count=%d\n", p.Name(), p.Count())
}
// 輸出: allocs / block / goroutine / heap / mutex / threadcreate

注意go test -cpuprofile 已開啟 CPU profiling,再呼叫 pprof.StartCPUProfile 會回傳 error,要判斷跳過。-trace 同理。

net/http/pprof

用途:

  • 幫 HTTP 服務掛上 /debug/pprof/*
import (
    "net/http"
    _ "net/http/pprof" // 副作用 import 即可註冊路由
)

func main() {
    go http.ListenAndServe(":6060", nil)
    // ... 你的程式 ...
}

常見 endpoint:

路徑用途
/debug/pprof/總覽
/debug/pprof/profile?seconds=30CPU 30 秒
/debug/pprof/heapheap 快照
/debug/pprof/goroutinegoroutine 堆疊
/debug/pprof/blockblock profile
/debug/pprof/mutexmutex profile
/debug/pprof/trace?seconds=5execution trace

注意:

  • block 需要 runtime.SetBlockProfileRate(1)
  • mutex 需要 runtime.SetMutexProfileFraction(1)

go tool pprof

用途:

  • 讀 profile 並做文字/圖形分析
# 讀本機 profile 檔
go tool pprof cpu.out
go tool pprof mem.out

# 直接從 HTTP 抓(程式要先跑起來)
go tool pprof http://127.0.0.1:6060/debug/pprof/heap
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30

# 開啟瀏覽器互動 UI(需要 graphviz)
go tool pprof -http=:8080 cpu.out

互動模式常用命令:

命令用途
top看最熱函式(flat/cum)
top10只看前幾名
list Foo對照原始碼,看哪幾行最熱
web開呼叫圖(需 graphviz)
peek Foo看 callers/callees
traces看樣本堆疊
svg輸出 SVG 呼叫圖

實測輸出(CPU profile)

go test -bench=BenchmarkCountPrimes -cpuprofile=cpu.out
go tool pprof -top cpu.out
Type: cpu  Duration: 1.30s  Samples = 1.17s (89.72%)
      flat  flat%   sum%        cum   cum%
     0.95s 81.20% 81.20%      1.03s 88.03%  go-tools-demo.isPrime (inline)
     0.12s 10.26% 91.45%      1.15s 98.29%  go-tools-demo.BenchmarkCountPrimes
     0.08s  6.84% 98.29%      0.08s  6.84%  math.Sqrt (inline)

解讀:

  • flat:函式自己花的時間(不含被呼叫者)
  • cum:函式 + 所有被呼叫者加起來的時間
  • (inline):已被 inline,不是真正的函式呼叫

實測輸出(Memory profile)

go test -bench=BenchmarkAllocStrings -memprofile=mem.out
go tool pprof -top mem.out
Type: alloc_space
      flat  flat%   sum%        cum   cum%
  654.21MB 69.55% 69.55%   932.72MB 99.16%  go-tools-demo.allocStrings
     278MB 29.56% 99.11%   278.51MB 29.61%  fmt.Sprintf

go tool pprofgo tool trace 怎麼選

工具主要回答的問題
pprof哪些函式最吃 CPU / memory / lock / block
trace為什麼 goroutine 在那個時間點沒跑、被誰卡住、GC/排程如何互動

一句話:

  • 熱點先看 pprof
  • 時序、排程、阻塞因果看 trace

4. Trace 全家桶

runtime/trace

用途:

  • 在程式中產生 execution trace
import "runtime/trace"

f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// ... 你的工作 ...

標註業務 task / region(方便在 UI 中對齊):

import "runtime/trace"

ctx, task := trace.NewTask(context.Background(), "processRequest")
defer task.End()

trace.WithRegion(ctx, "parse", func() {
    // 解析邏輯
})

trace.Log(ctx, "db", "query start")

什麼時候用:

  • 想看 goroutine 建立、喚醒、阻塞、syscall、GC、P/M/G 排程
  • 不只想知道「熱點」,還要知道「為什麼這個時間點卡住」

go tool trace

go test ./... -run TestTrace -trace=trace.out
go tool trace trace.out
# 開啟瀏覽器 http://127.0.0.1:<隨機port>

# 或從 HTTP 服務抓
curl -o trace.out http://127.0.0.1:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

UI 頁面說明:

頁面看什麼
View trace時間軸,所有 goroutine/P/GC 事件
Goroutine analysis各 goroutine 排程延遲統計
Network blockingnetwork 等待時間
Synchronization blockingchannel/mutex 等待時間
Syscall blockingsyscall 阻塞時間
Scheduler latencygoroutine 從 runnable 到 running 的延遲
User-defined taskstrace.NewTask 標記的業務任務

優勢:有時間軸、有 goroutine 與 task 關聯、看得出 block/unblock 因果

限制:檔案可能很大;trace overhead 通常比 pprof 高


5. Coverage 工具

go test -cover / -coverprofile

# 快速看覆蓋率百分比
go test ./... -cover
# ok  demo  0.255s  coverage: 50.0% of statements

# 輸出詳細 profile
go test ./... -coverprofile=cover.out

go tool cover

# 每個函式的覆蓋率
go tool cover -func=cover.out
# main.go:13:  isPrime      100.0%
# main.go:25:  countPrimes  100.0%
# main.go:73:  main           0.0%
# total:       (statements)  50.0%

# 開 HTML 報告(瀏覽器高亮顯示哪些行沒測到)
go tool cover -html=cover.out

go tool covdata(Go 1.20+)

用途:

  • 操作新版 coverage data 檔案
  • 整合多次執行、多個 binary 的 coverage
# 收集可執行檔的 coverage
GOCOVERDIR=/tmp/cov ./myapp
go tool covdata textfmt -i=/tmp/cov -o=cover.out
go tool cover -html=cover.out

6. Race / Sanitizer / Memory 問題

-race

go test ./... -race
go build -race -o myapp .

代價:慢 2~20x、binary 變大。建議只在 CI 或排查時開。

有意識地製造 race(反面教材):

// 這段程式碼沒有鎖,會被 -race 抓到
var counter int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++ // DATA RACE
    }()
}
wg.Wait()

正確做法:用 sync.Mutex 或 atomic:

// 方法一:Mutex(上面的 Counter 示範)
// 方法二:atomic
var counter int64
go func() { atomic.AddInt64(&counter, 1) }()

-asan

go build -asan -o myapp .

適用:cgo 程式懷疑 native memory 越界、use-after-free。

-msan

go build -msan -o myapp .

適用:cgo 程式懷疑未初始化記憶體。


7. Runtime 觀測與控制

GODEBUG

格式:

GODEBUG=key=value,key=value ./app

常用 GODEBUG 選項

變數用途什麼時候用
gctrace=1每次 GC 印一行摘要先看 GC 是否異常頻繁、pause 是否過大
schedtrace=1000每 1000ms 印 scheduler 摘要看 runnable goroutine 是否堆積
scheddetail=1配合 schedtrace 印更多細節scheduler 問題深入分析
inittrace=1印每個 package init 時間/配置量啟動慢、初始化太肥
scavtrace=1印 scavenger 資訊看記憶體回收給 OS 的行為
gcpacertrace=1印 GC pacer 狀態深入 GC 調校
allocfreetrace=1每次 alloc/free 都列 trace小範圍重現可疑 allocation 問題,代價極高
clobberfree=1free 後覆寫壞值查 use-after-free 類問題
efence=1每個物件單獨 page 配置查 allocator / memory smash 類問題
cgocheck=2cgo 指標傳遞重檢查懷疑 Go pointer 被錯誤交給 C
madvdontneed=0/1控制記憶體歸還 OS 策略查 RSS 不降問題
memprofilerate=1調整記憶體 profile 取樣率要更精準 allocation profile
tracebackancestors=Ntraceback 顯示 goroutine 祖先查 goroutine 是誰生出來的
asyncpreemptoff=1關閉 async preemption查 preemption / GC 相關疑難雜症
gcstoptheworld=1關閉 concurrent GC對照 GC 行為,不建議常態使用

gctrace=1 輸出解讀

gc 7 @0.123s 2%: 0.1+0.5+0.03 ms clock, 0.8+0.4/0.3/0+0.2 ms cpu, 4->4->2 MB, 5 MB goal, 16 P
^   ^  ^     ^   ^              ^                                  ^         ^
GC# 時間戳 佔比 STW+並發+STW  CPU時間            heap: before->after->live  goal P數

常見命令

GODEBUG=gctrace=1 ./app
GODEBUG=schedtrace=1000,scheddetail=1 ./app
GODEBUG=inittrace=1 ./app
GODEBUG=gctrace=1,scavtrace=1 ./app

GOGC

用途:控制 GC 目標比例,預設 100(即 heap 增長到上次存活大小的 2 倍時觸發 GC)。

GOGC=50 ./app    # 更積極 GC,記憶體壓力大時用
GOGC=200 ./app   # 少 GC,記憶體夠多時用
GOGC=off ./app   # 完全關閉 GC(危險,只用於診斷)

程式內動態調整:

old := debug.SetGCPercent(50)
defer debug.SetGCPercent(old)

GOMEMLIMIT(Go 1.19+)

用途:設定 Go runtime soft memory limit。

GOMEMLIMIT=512MiB ./app
GOMEMLIMIT=1GiB ./app

適用場景:

  • 容器內記憶體有限,避免 OOM Kill
  • 搭配 GOGC=off 讓 GC 只由記憶體上限觸發,減少 GC 次數

程式內設定:

import "runtime/debug"
debug.SetMemoryLimit(512 * 1024 * 1024) // 512 MiB

GOMAXPROCS

GOMAXPROCS=4 ./app   # 限制只用 4 個 CPU

程式內設定:

runtime.GOMAXPROCS(4)
fmt.Println(runtime.GOMAXPROCS(0)) // 查詢當前值

GORACE

配合 -race 使用:

GORACE="log_path=/tmp/race.log halt_on_error=1" go test ./... -race

GOTRACEBACK

效果
none幾乎不印 stack
single只印當前 goroutine
all印所有 user goroutine
system連 runtime goroutine 一起印
crash印完後觸發系統層 crash/core dump
GOTRACEBACK=all ./app
GOTRACEBACK=crash ./app  # 搭配 ulimit -c unlimited 產生 core dump

8. runtime/metricsruntime/debug

runtime/metrics(Go 1.16+)

用途:穩定地讀取 runtime 指標(比 MemStats 更好的 API)。

import "runtime/metrics"

// 查詢所有可用指標
descs := metrics.All()
for _, d := range descs {
    fmt.Printf("%s: %s\n", d.Name, d.Description)
}

// 讀取特定指標
samples := []metrics.Sample{
    {Name: "/gc/heap/allocs:bytes"},
    {Name: "/gc/cycles/total:gc-cycles"},
    {Name: "/memory/classes/heap/objects:bytes"},
    {Name: "/sched/latencies:seconds"},
}
metrics.Read(samples)
for _, s := range samples {
    fmt.Printf("%s = %v\n", s.Name, s.Value)
}

常用指標名稱:

指標意義
/gc/heap/allocs:bytes累計分配 bytes
/gc/heap/frees:bytes累計釋放 bytes
/gc/cycles/total:gc-cyclesGC 次數
/gc/pauses/total/other:secondsGC pause 時間
/memory/classes/heap/objects:bytes活躍 heap 物件大小
/sched/latencies:secondsgoroutine 排程延遲分佈
/cpu/classes/user:cpu-secondsuser CPU 使用時間

適合:exporter、/metrics endpoint、長期監控儀表板。

runtime/debug

import "runtime/debug"

// 動態調整 GC
old := debug.SetGCPercent(50)
defer debug.SetGCPercent(old)

// 設定記憶體上限(等同 GOMEMLIMIT)
debug.SetMemoryLimit(512 * 1024 * 1024)

// 主動把記憶體還給 OS
debug.FreeOSMemory()

// 讀 GC 統計
var stats debug.GCStats
debug.ReadGCStats(&stats)
fmt.Printf("NumGC=%d, LastGC=%v\n", stats.NumGC, stats.LastGC)
fmt.Printf("Pause total: %v\n", stats.PauseTotal)

// 讀 build info(module 路徑、版本、依賴)
if info, ok := debug.ReadBuildInfo(); ok {
    fmt.Printf("module: %s\n", info.Main.Path)
    for _, dep := range info.Deps {
        fmt.Printf("  dep: %s %s\n", dep.Path, dep.Version)
    }
}

runtime.MemStats

var ms runtime.MemStats
runtime.GC()                   // 先 GC 讓數字準確
runtime.ReadMemStats(&ms)

fmt.Printf("HeapAlloc:   %d KB\n", ms.HeapAlloc/1024)   // 當前活躍 heap
fmt.Printf("TotalAlloc:  %d KB\n", ms.TotalAlloc/1024)  // 累計分配(只增不減)
fmt.Printf("Sys:         %d KB\n", ms.Sys/1024)          // 從 OS 取得的總記憶體
fmt.Printf("NumGC:       %d\n", ms.NumGC)                // GC 次數
fmt.Printf("PauseTotalNs:%d ns\n", ms.PauseTotalNs)      // GC 累計 pause 時間

9. 低階 go tool 完整列表

工具用途什麼時候用
addr2line位址轉原始碼位置有 PC 位址、stack 位址,要反查檔案/行號
asmGo 組譯器.s、研究底層 ABI,平時很少直接用
buildid讀/寫 build ID排查 binary 是否對應正確 build
cgo產生 Go/C 互通膠水碼cgo 除錯、研究產生碼
compileGo 編譯器極低階分析、看編譯器行為
covdata操作 coverage data整合多份 coverage
covercoverage 報告/HTML看哪些行沒測到
distGo 發行版/建置相關工具鏈開發、少數情境
distpack發行打包工具幾乎只有 Go 發行流程會碰
doc文件查詢低階入口一般直接用 go doc 即可
fixAPI 遷移修正低階入口一般直接用 go fix
linkGo linker研究連結流程、極低階分析
nm列 symbol tablebinary 符號分析、大小、是否被裁剪
objdump反組譯查 hot path 最終機器碼
pack操作 archive極少直接使用
pprofprofile 分析CPU / memory / block / mutex 熱點分析
test2json測試輸出轉 JSONCI、IDE、機器分析
traceexecution trace UI排程、GC、阻塞因果分析
vet靜態檢查低階入口一般直接用 go vet

go tool nm 範例

go build -o myapp .
go tool nm myapp | grep "main\."

輸出(實測):

483a80 T main.(*Counter).Get
483a00 T main.(*Counter).Inc
483900 T main.allocStrings
483ba0 T main.main

欄位:地址 類型 符號名T = text(程式碼段)

go tool objdump 範例

go tool objdump -s "main\.allocStrings" myapp | head -15

輸出(實測):

TEXT main.allocStrings(SB) /path/to/main.go
  main.go:36    CMPQ SP, 0x10(R14)
  main.go:36    JBE  ...
  main.go:37    MOVQ AX, BX
  main.go:37    LEAQ 0x859c(IP), AX
  main.go:37    CALL runtime.makeslice(SB)

go tool addr2line 範例

# 把 PC 位址轉成 file:line
echo "0x483900" | go tool addr2line myapp

go doc 範例

go doc runtime.MemStats
go doc sync.Mutex.Lock
go doc fmt.Sprintf
go doc -all net/http

go tool test2json 範例

go test ./... -v | go tool test2json | jq '.Action'
# "run" "output" "pass" "fail" ...

10. 實戰選型

CPU 高

先用:

go test ./... -bench=. -cpuprofile=cpu.out
go tool pprof cpu.out
# 互動輸入 top10,看 flat% 最高的函式

如果是線上服務:

go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30

記憶體高

先用:

go test ./... -run TestX -memprofile=mem.out
go tool pprof mem.out
# 互動輸入 top,看 alloc_space 最高的函式

再搭配:

GODEBUG=gctrace=1 ./app

如果是容器壓力:

  • GOMEMLIMIT
  • GOGC
  • runtime/metrics

鎖競爭 / 卡住

先用:

go test ./... -run TestX -mutexprofile=mutex.out -mutexprofilefraction=1
go tool pprof mutex.out

如果要看卡住時序:

go test ./... -run TestX -trace=trace.out
go tool trace trace.out

Goroutine 洩漏 / 誰沒結束

先用:

# 線上服務
go tool pprof http://127.0.0.1:6060/debug/pprof/goroutine

# 非 HTTP 服務(程式內抓)
f, _ := os.Create("goroutine.out")
pprof.Lookup("goroutine").WriteTo(f, 1) // 1 = text 格式
f.Close()

# panic 時印全部 goroutine
GOTRACEBACK=all ./app

啟動慢

先用:

GODEBUG=inittrace=1 ./app

輸出格式:init pkg=xxx @t ms, 2 ms clock, 1000 bytes, 3 allocs

如果還要看整段啟動時序:

go test -run TestStartup -trace=trace.out
go tool trace trace.out

疑似資料競態

直接先用:

go test ./... -race

不要一開始就先看 pprof。那是不同維度的問題。

想看 inline / escape 決策

go build -gcflags='-m' ./...           # 一層 inline 資訊
go build -gcflags='-m -m' ./...        # 更詳細
go build -gcflags='all=-m' ./...       # 包含依賴

11. 一句話版心法

問題類型最優先工具
熱點pprof
時序/阻塞/排程trace
競態-race
測試覆蓋率cover
runtime 內部狀態GODEBUG
GC/記憶體策略GOGCGOMEMLIMITruntime/metrics
低階 binary/匯編objdumpnmaddr2line

12. 建議工作流

效能問題排查,通常照這順序最省時間:

  1. 先重現問題(加 -race 先排掉競態)
  2. CPU 問題先抓 pprof-cpuprofile + go tool pprof -top
  3. 卡頓/延遲/阻塞再補 trace-trace + go tool trace
  4. 記憶體問題加 memprofilegctrace
  5. 並發正確性問題跑 -race
  6. 需要長期監控時接 runtime/metrics

不要反過來:

  • 不要一上來就開一堆 GODEBUG
  • 不要還沒 benchmark 就急著看匯編
  • 不要把 pproftrace 當同一種工具
  • 不要在生產環境一開始就跑 allocfreetrace=1(overhead 極高)

13. 快速參考:profile 種類對照

Profile 種類收集方式讀取方式看什麼
CPU-cpuprofile / pprof.StartCPUProfilego tool pprof -top函式 CPU 佔比
Heap-memprofile / pprof.WriteHeapProfilego tool pprof -topallocation 大頭
Block-blockprofile + rate=1 / Lookup("block")go tool pprof -topchannel/mutex/syscall 阻塞
Mutex-mutexprofile + fraction=1 / Lookup("mutex")go tool pprof -top鎖競爭熱點
GoroutineLookup("goroutine").WriteTo(f,1)直接看文字 / pprofgoroutine 堆疊快照
Trace-trace / trace.Startgo tool trace時序、排程、GC 全景
Coverage-coverprofilego tool cover哪些行未被測試覆蓋