本文 AI 產出,尚未審核

Golang 課程 – 錯誤處理與測試

主題:基準測試(Benchmark)


簡介

在軟體開發的全生命週期中,效能往往是決定系統能否成功的關鍵因素之一。Go 語言內建的測試框架不只支援單元測試 (Test*),還提供了 基準測試(benchmark),讓開發者能以科學、可重複的方式測量程式碼的執行速度與資源使用情形。

基準測試的價值不只在於找出「哪段程式碼太慢」;它同時也是 性能回歸測試 的重要工具,能在功能調整或重構後即時驗證效能是否維持在預期範圍。對於初學者而言,掌握基準測試的寫法與最佳實踐,能在日後開發大型服務或高併發系統時,避免因效能問題導致的瓶頸與維護成本。

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整介紹 Go 的基準測試,讓你能 快速上手、正確使用,並在程式碼品質上更上一層樓。


核心概念

1. 基準測試的基本結構

Go 的基準測試函式必須以 Benchmark 為前綴,接受 *testing.B 參數:

func BenchmarkXXX(b *testing.B) {
    // b.N 代表測試框架自動決定的迭代次數
    for i := 0; i < b.N; i++ {
        // 被測試的程式碼
    }
}
  • b.N:測試框架會根據前一次執行的時間自動調整 N,以確保測試時間足夠(通常在 1 秒以上),從而得到較穩定的測量結果。
  • b.ResetTimer():在需要排除前置作業(如資料初始化)的時間時,可呼叫此方法重置計時器。
  • b.StopTimer() / b.StartTimer():可在基準測試中暫停與重新啟動計時,適用於需要額外清理或設定的情境。

2. 設定基準測試的參數

testing 套件支援 -bench-benchmem-run 等旗標:

旗標 功能
-bench=. 執行所有基準測試
-bench=BenchmarkName 只跑指定名稱的基準測試
-benchmem 顯示每次迭代的記憶體配置(alloc、bytes)
-benchtime=5s 設定每個基準測試的執行時間上限(預設 1s)

範例指令:

go test -bench=. -benchmem -benchtime=3s

3. 基準測試與平行執行

若要測試 併發安全多核效能,可使用 b.RunParallel

func BenchmarkParallelSort(b *testing.B) {
    data := generateRandomSlice(1000)
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            sort.Ints(data) // 只要確保每個 goroutine 使用自己的副本
        }
    })
}

RunParallel 會自動根據 GOMAXPROCS 建立適當數量的 goroutine,讓每個 goroutine 執行 pb.Next() 迴圈,模擬高併發情境。

4. 基準測試的結果解讀

執行 go test -bench=. -benchmem 後,會得到類似以下的輸出:

BenchmarkSum-8        10000000               123 ns/op            0 B/op          0 allocs/op
BenchmarkConcat-8     2000000               567 ns/op          112 B/op          2 allocs/op
  • ns/op:每次迭代所耗費的納秒數。
  • B/op:每次迭代分配的位元組數(僅在使用 -benchmem 時顯示)。
  • allocs/op:每次迭代的記憶體分配次數。

5. 基準測試與自訂計時

有時候想測量 整段程式碼的執行時間(包含前置與後置作業),可自行使用 time.Now(),但要注意 不要混用 b.N 與自行計時,否則會失去框架自動調整迭代次數的優勢。

func BenchmarkManualTimer(b *testing.B) {
    start := time.Now()
    for i := 0; i < b.N; i++ {
        heavyComputation()
    }
    elapsed := time.Since(start)
    b.ReportMetric(float64(elapsed.Nanoseconds())/float64(b.N), "ns/custom")
}

程式碼範例

以下提供 5 個實用範例,涵蓋基礎、記憶體、平行與自訂計時等情境。

範例 1:簡單的加總函式

// sum.go
package demo

func Sum(nums []int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
// sum_test.go
package demo

import "testing"

func BenchmarkSum(b *testing.B) {
    // 建立固定長度的測試資料
    data := make([]int, 1000)
    for i := range data {
        data[i] = i
    }

    b.ResetTimer() // 排除資料建立時間
    for i := 0; i < b.N; i++ {
        Sum(data)
    }
}

重點:使用 b.ResetTimer() 讓基準測試只計算 Sum 本身的執行時間。

範例 2:字串拼接 vs. bytes.Buffer

package demo

import (
    "bytes"
    "strings"
)

func ConcatPlus(a, b string) string {
    return a + b
}

func ConcatBuffer(a, b string) string {
    var buf bytes.Buffer
    buf.WriteString(a)
    buf.WriteString(b)
    return buf.String()
}
func BenchmarkConcatPlus(b *testing.B) {
    a, c := "hello", "world"
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatPlus(a, c)
    }
}

func BenchmarkConcatBuffer(b *testing.B) {
    a, c := "hello", "world"
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ConcatBuffer(a, c)
    }
}

觀察:執行 go test -bench=. -benchmem 後,可比較兩種實作的 分配次數記憶體使用量,選擇最適合的實作方式。

範例 3:使用 RunParallel 測試併發安全的 map

package demo

import "sync"

func ParallelWrite(m *sync.Map, key, value int) {
    m.Store(key, value)
}
func BenchmarkParallelMap(b *testing.B) {
    var m sync.Map
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ParallelWrite(&m, 1, 2)
        }
    })
}

說明sync.Map 本身支援併發寫入,透過 RunParallel 可驗證在高併發下的效能表現。

範例 4:測量大量資料排序的效能

package demo

import (
    "math/rand"
    "sort"
    "time"
)

func RandomInts(n int) []int {
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    s := make([]int, n)
    for i := range s {
        s[i] = r.Intn(1000000)
    }
    return s
}
func BenchmarkSort1000(b *testing.B) {
    data := RandomInts(1000) // 只產生一次
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        // 必須複製資料,避免前一次排序影響結果
        tmp := make([]int, len(data))
        copy(tmp, data)
        sort.Ints(tmp)
    }
}

技巧:在基準測試中 避免共享可變資料,否則會產生不一致的測量結果。

範例 5:自訂計時與報告額外指標

func BenchmarkCustomMetric(b *testing.B) {
    start := time.Now()
    for i := 0; i < b.N; i++ {
        heavyComputation()
    }
    elapsed := time.Since(start)
    // 每次迭代的平均耗時(自訂指標)
    b.ReportMetric(float64(elapsed.Nanoseconds())/float64(b.N), "ns/custom")
}

應用:當需要追蹤 非標準指標(例如 I/O 次數、網路請求數)時,可利用 ReportMetric 手動上報。


常見陷阱與最佳實踐

陷阱 說明 解法
忘記使用 b.ResetTimer() 前置資料產生會被算入基準時間,導致結果失真。 在所有一次性初始化完成後立即呼叫 b.ResetTimer()
在迴圈內部分配大量記憶體 每次迭代都會產生 GC 壓力,測試結果會被記憶體分配干擾。 盡量在迭代外部建立緩衝區,或使用 sync.Pool
共享可變資料 多個迭代同時使用同一 slice/map,會產生競爭條件或排序結果不一致。 為每次迭代建立獨立的副本(copyappend([]T{}, src...))。
基準測試過於簡單 測試只跑了極少次數,誤差大。 go test 自動調整 b.N,或使用 -benchtime 增加測試時間。
忽略 -benchmem 只看時間指標,卻忽略了記憶體分配的成本。 加上 -benchmem,同時關注 ns/opB/opallocs/op

最佳實踐

  1. 保持基準測試的獨立性:每個 Benchmark* 應只測試單一功能或單一變化,避免混雜多個因素。
  2. 使用 testing.B 的輔助方法b.StopTimer()b.StartTimer()b.ReportAllocs() 能幫助精細控制測試範圍。
  3. 在 CI/CD 中加入基準測試:將基準測試納入自動化流程,配合閾值(threshold)檢查,防止效能倒退。
  4. 以真實工作負載為基礎:測試資料大小、結構應盡可能模擬實際生產環境,才能得到具參考價值的結果。
  5. 記錄與追蹤結果:將基準測試的輸出寫入檔案或 Grafana 等監控系統,方便長期趨勢分析。

實際應用場景

場景 為何需要基準測試 可能的測試項目
Web API 回傳 JSON 高併發時 JSON 序列化可能成為瓶頸 json.Marshaljson.Encoder 的效能比較
資料庫批次寫入 大量寫入時每筆資料的轉換成本會累積 sql.Txpgx.CopyFrom 的基準測試
影像處理或壓縮 CPU 密集型演算法需確保在可接受的延遲內 image/jpeg.Encode、自訂演算法的時間測量
Cache 讀寫 讀取快取與寫入快取的效能直接影響整體響應時間 sync.Mapristretto.Cachebigcache 的基準測試
微服務間 RPC 序列化、壓縮與網路傳輸的總體延遲需要量測 grpcprotobufmsgpack 的編解碼時間

透過上述基準測試,開發團隊可以在 設計階段 立即發現效能瓶頸,或在 版本升級 時快速驗證新實作是否維持或提升效能。


總結

  • 基準測試是 Go 測試套件中 不可或缺 的功能,能以科學方法量測程式碼的執行時間與記憶體分配。
  • 正確使用 b.Nb.ResetTimer()b.RunParallel() 等 API,能避免常見的測試失真問題。
  • 透過 -benchmemReportMetric 等技巧,我們不只看時間,還能掌握 記憶體與資源使用 的全貌。
  • 在實務開發中,將基準測試納入 CI/CD效能門檻長期監控,才能確保系統在不斷演進的同時,保持良好的效能表現。

最後的建議:從現在開始,為每個重要的演算法或 I/O 操作寫下基準測試,並把測試結果當作 性能指標 來管理。只要養成這個習慣,未來面對效能優化時,你將不再手忙腳亂,而是有據可依、一步步提升系統的表現。祝你在 Go 的世界裡,寫出既 正確高效 的程式碼!