本文 AI 產出,尚未審核

Golang – 錯誤處理與測試

主題:範例測試(Example Tests)


簡介

在 Go 語言的測試生態系統中,範例測試(Example tests) 是一個常被忽略卻相當實用的功能。它不僅能夠作為文件的使用說明,還能在 go test 時自動驗證範例程式碼的正確性。對於想要提供 易讀、可執行的文件,或是希望在 CI 流程中檢查範例是否失效的開發者來說,掌握範例測試是提升程式碼品質與使用者體驗的關鍵一步。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到實務應用場景,帶領 初學者到中級開發者 完整了解並善用 Go 的 Example 測試。


核心概念

1. 什麼是 Example 測試?

  • Example 測試 是放在 *_test.go 檔案中的特殊函式,名稱必須以 Example 開頭,例如 ExampleHelloWorld
  • 它同時具備 文件說明(在 godoc 中會顯示)與 測試驗證(執行時會比對輸出)兩大功能。
  • 若函式內部有 // Output:// Unordered output: 註解,go test 會將函式的標準輸出與註解內容比較,若不相符則測試失敗。

2. 為什麼要使用 Example 測試?

好處 說明
同步文件與程式碼 範例直接寫在測試檔,文件不會因為程式碼變更而過時。
自動驗證 go test 會執行範例並檢查輸出,避免手動測試時的遺漏。
提升可讀性 使用者在閱讀文件時可看到可直接執行的程式碼片段。
支援 CI/CD 範例失效會直接導致測試失敗,讓 CI 能即時捕捉問題。

3. 基本語法與規則

func Example<FunctionName>() {
    // 範例程式碼
    fmt.Println("Hello, World!")
    // Output:
    // Hello, World!
}
  • 函式名稱必須以 Example 開頭,後面可以接要說明的函式或型別名稱(可省略)。
  • // Output: 後面的文字必須精確匹配函式執行時的輸出(包括換行)。
  • 若輸出順序不固定,可使用 // Unordered output:go test 會把每一行視為集合比較。
  • 若不想驗證輸出,只要省略 // Output: 註解即可,該範例仍會在文件中顯示。

程式碼範例

以下提供 5 個常見且實用的 Example 測試範例,每個範例都附上說明與注意點。

範例 1:最簡單的 Hello World

package hello_test

import (
    "fmt"
)

func ExampleHelloWorld() {
    fmt.Println("Hello, World!")
    // Output:
    // Hello, World!
}

說明:最基礎的範例,展示了 // Output: 的寫法。若執行 go test,測試會通過,且 godoc 會把此程式碼顯示在文件中。


範例 2:使用自訂函式與錯誤處理

package mathutil_test

import (
    "fmt"
    "math"
)

// Divide 回傳 a / b,若 b 為 0 則回傳錯誤
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func ExampleDivide() {
    // 正常除法
    result, _ := Divide(10, 2)
    fmt.Println(result)

    // 除以 0 的錯誤情況
    _, err := Divide(5, 0)
    fmt.Println(err)

    // Output:
    // 5
    // division by zero
}

重點:即使函式回傳 error,只要把錯誤列印出來,// Output: 仍能捕捉。這樣的範例同時說明了 正確路徑錯誤路徑


範例 3:測試 JSON 序列化(使用 Unordered output)

package jsonutil_test

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func ExampleMarshalPerson() {
    p := Person{Name: "Alice", Age: 30}
    data, _ := json.Marshal(p)
    fmt.Println(string(data))

    // 輸出欄位順序在不同 Go 版本可能不同,使用 Unordered output
    // Unordered output:
    // {"age":30,"name":"Alice"}
    // {"name":"Alice","age":30}
}

說明json.Marshal 的欄位順序在不同環境可能不一致,使用 // Unordered output: 可以避免測試因順序差異而失敗。


範例 4:示範介面實作與多型

package shape_test

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct{ Radius float64 }

func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 { return r.Width * r.Height }

func ExampleShapeArea() {
    shapes := []Shape{
        Circle{Radius: 2},
        Rectangle{Width: 3, Height: 4},
    }

    for _, s := range shapes {
        fmt.Printf("%.2f\n", s.Area())
    }

    // Output:
    // 12.57
    // 12.00
}

技巧:範例中同時展示了 介面結構體多型 的使用方式,對於想快速了解概念的讀者非常友好。


範例 5:測試 testing 套件的 Run 子測試(示範 t.Run

package subtest_test

import (
    "testing"
)

func TestParent(t *testing.T) {
    t.Run("子測試 A", func(t *testing.T) {
        t.Log("執行 A")
    })
    t.Run("子測試 B", func(t *testing.T) {
        t.Log("執行 B")
    })
}

// ExampleTestParent 展示子測試的執行結果
func ExampleTestParent() {
    // 直接執行 go test -run TestParent -v
    // 這裡僅示意輸出格式
    // Output:
    // === RUN   TestParent
    // === RUN   TestParent/子測試_A
    // --- PASS: TestParent/子測試_A (0.00s)
    // === RUN   TestParent/子測試_B
    // --- PASS: TestParent/子測試_B (0.00s)
    // --- PASS: TestParent (0.00s)
}

說明:雖然 Example 主要用於說明程式碼,但也可以展示測試本身的執行結果,讓使用者了解 t.Run 的行為。


常見陷阱與最佳實踐

陷阱 說明 解決方案
輸出不一致 fmt.Println 會自動在結尾加換行,若忘記在 // Output: 加上換行會失敗。 確認 // Output: 每一行都與實際輸出完全相同(包括空格與換行)。
隨機順序 mapjsonset 等資料結構的遍歷順序不固定。 使用 // Unordered output: 或先排序後輸出。
測試時間過長 範例測試會在 go test 時執行,若裡面有 I/O 或長時間運算會拖慢測試。 只放 簡潔、快速 的示範;將耗時邏輯抽離至一般測試或 benchmark。
依賴外部資源 讀寫檔案、網路請求等會讓範例不具備可重現性。 使用 mock 或在範例中說明「此處僅示意」並避免實際呼叫外部服務。
未使用 t 參數 若在 Example 中需要測試失敗訊息,卻忘記使用 t.Fatalf,會導致測試無法捕捉錯誤。 若需要 testing.T,可改寫成 func ExampleFoo(t *testing.T)(Go 1.20 起支援),或使用 t.Log 只作說明。

最佳實踐

  1. 保持範例簡潔:每個 Example 只聚焦於單一概念或 API。
  2. 同步文件與程式碼:將範例直接寫在 *_test.go,避免文件與實作脫節。
  3. 使用 Unordered output:對於非確定性輸出,盡量使用此標記。
  4. 加入註解說明:在程式碼上方或旁邊加上 // Output: 前的說明,提升可讀性。
  5. 在 CI 中執行:確保 go test ./... 包含所有模組,範例失效會直接導致 CI 失敗。

實際應用場景

1. 開源函式庫的文件說明

  • 情境:你正在維護一個公開的 Go 套件(例如 github.com/yourname/awesomepkg),希望使用者能快速了解每個函式的用法。
  • 做法:在 awesomepkg 的每個公共函式旁邊建立 example_test.go,寫上 Example<FunctionName>,並在 godoc 中自動呈現。
  • 好處:使用者在閱讀文件時即看到可執行的範例,且每次套件更新時範例會自動驗證,避免文件過時。

2. 內部服務的 API 文檔

  • 情境:公司內部有多個微服務,彼此透過 gRPC 或 HTTP JSON 介面互動。
  • 做法:在每個服務的 proto 生成的 Go 檔案旁加入 Example,示範如何建立客戶端、呼叫方法以及解析回應。
  • 好處:新進工程師只要跑 go test ./... 就能看到範例是否仍然可用,減少學習曲線。

3. 教學課程與部落格

  • 情境:你在寫一本 Go 語言的教學書或部落格文章,需要提供可直接貼上執行的程式碼。
  • 做法:把所有示範程式碼寫成 Example 測試,然後在文章中直接引用 godoc 產生的程式碼片段。
  • 好處:讀者若自行下載原始碼,執行 go test 就能驗證文章的正確性,提升教學品質。

4. CI/CD 中的回歸測試

  • 情境:你在 CI pipeline 中執行 go test -run Example,只跑範例測試,以快速檢查 API 變更是否破壞既有範例。
  • 做法:在 go.mod 中設定 go test ./... -run ^Example,讓 CI 只跑 Example 測試。
  • 好處:即使功能測試較慢,範例測試仍能在幾秒內完成,提供快速回饋。

總結

  • Example 測試 是 Go 測試框架中兼具文件說明與自動驗證的利器。
  • 正確使用 // Output:// Unordered output:,能讓範例在 go test 時即時檢查正確性,避免文件與程式碼脫節。
  • 在撰寫範例時,保持 簡潔、可重現,避免依賴外部資源或產生長時間運算。
  • 透過最佳實踐(如排序輸出、使用 mock、加入說明註解),可以減少常見陷阱,提升範例的可靠度。
  • 開源套件、內部 API、教學教材與 CI/CD 等多種情境下,範例測試都能發揮關鍵作用,幫助團隊維持高品質、易上手的程式碼基礎。

實務建議:在每個新功能完成後,先寫一個對應的 Example 測試,確保文件與程式碼同步。長期下來,你會發現範例測試不僅減少了文件維護成本,也成為了自動化測試的重要補強。

祝你在 Golang 的錯誤處理與測試之路上,寫出更乾淨、更可靠的程式碼! 🚀