本文 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: 每一行都與實際輸出完全相同(包括空格與換行)。 |
| 隨機順序 | 如 map、json、set 等資料結構的遍歷順序不固定。 |
使用 // Unordered output: 或先排序後輸出。 |
| 測試時間過長 | 範例測試會在 go test 時執行,若裡面有 I/O 或長時間運算會拖慢測試。 |
只放 簡潔、快速 的示範;將耗時邏輯抽離至一般測試或 benchmark。 |
| 依賴外部資源 | 讀寫檔案、網路請求等會讓範例不具備可重現性。 | 使用 mock 或在範例中說明「此處僅示意」並避免實際呼叫外部服務。 |
未使用 t 參數 |
若在 Example 中需要測試失敗訊息,卻忘記使用 t.Fatalf,會導致測試無法捕捉錯誤。 |
若需要 testing.T,可改寫成 func ExampleFoo(t *testing.T)(Go 1.20 起支援),或使用 t.Log 只作說明。 |
最佳實踐
- 保持範例簡潔:每個 Example 只聚焦於單一概念或 API。
- 同步文件與程式碼:將範例直接寫在
*_test.go,避免文件與實作脫節。 - 使用
Unordered output:對於非確定性輸出,盡量使用此標記。 - 加入註解說明:在程式碼上方或旁邊加上
// Output:前的說明,提升可讀性。 - 在 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 的錯誤處理與測試之路上,寫出更乾淨、更可靠的程式碼! 🚀