在 Golang 中使用 samber/oops 進行結構化錯誤處理 — 錯誤建構器、堆疊追蹤、錯誤代碼、錯誤上下文、錯誤包裝、錯誤屬性、使用者友善與開發者訊息、恐慌恢復以及日誌整合。適用於使用或採用 samber/oops 的情況,或當程式碼庫已匯入 github.com/samber/oops 時。
角色: 你是一位將錯誤視為結構化資料的 Go 工程師。每個錯誤都攜帶足夠的上下文 — 領域、屬性、追蹤 — 讓值班工程師無需詢問開發者即可診斷問題。
samber/oops 結構化錯誤處理
samber/oops 是 Go 標準錯誤處理的即插即用替代方案,增加了結構化上下文、堆疊追蹤、錯誤代碼、公開訊息和恐慌恢復。變數資料應放在 .With() 屬性中(而非訊息字串),以便 APM 工具(Datadog、Loki、Sentry)正確分組錯誤。與標準函式庫方法(在日誌位置添加 slog 屬性)不同,oops 屬性會隨著錯誤在呼叫堆疊中傳遞。
為什麼使用 samber/oops
標準 Go 錯誤缺乏上下文 — 你只看到 connection failed,但不知道是哪個使用者觸發、正在執行什麼查詢、或完整的呼叫堆疊。samber/oops 提供:
- 結構化上下文 — 任何錯誤上的鍵值屬性
- 堆疊追蹤 — 自動擷取呼叫堆疊
- 錯誤代碼 — 機器可讀的識別碼
- 公開訊息 — 與技術細節分開的使用者安全訊息
- 低基數訊息 — 變數資料放在
.With()屬性中,而非訊息字串,以便 APM 工具正確分組錯誤
此技能並非詳盡無遺。請參考函式庫文件和程式碼範例以獲取更多資訊。Context7 可作為探索平台提供協助。如需 Go 套件文件、版本、符號和已知漏洞,→ 請參閱 samber/cc-skills-golang@golang-pkg-go-dev 技能。
核心模式:錯誤建構器鏈
所有 oops 錯誤都使用流暢的建構器模式:
err := oops.
In("user-service"). // 領域/功能
Tags("database", "postgres"). // 分類
Code("network_failure"). // 機器可讀識別碼
User("user-123", "email", "foo@bar.com"). // 使用者上下文
With("query", query). // 自訂屬性
Errorf("failed to fetch user: %s", "timeout")
終端方法:
.Errorf(format, args...)— 建立新錯誤.Wrap(err)— 包裝現有錯誤.Wrapf(err, format, args...)— 包裝並附帶訊息.Join(err1, err2, ...)— 合併多個錯誤.Recover(fn)/.Recoverf(fn, format, args...)— 將恐慌轉換為錯誤
錯誤建構器方法
| 方法 | 使用情境 |
|---|---|
.With("key", value) |
新增自訂鍵值屬性(支援惰性 func() any 值) |
.WithContext(ctx, "key1", "key2") |
從 Go context 中提取值作為屬性(支援惰性值) |
.In("domain") |
設定功能/服務/領域 |
.Tags("auth", "sql") |
新增分類標籤(使用 err.HasTag("tag") 查詢) |
.Code("iam_authz_missing_permission") |
設定機器可讀的錯誤識別碼/代號 |
.Public("Could not fetch user.") |
設定使用者安全訊息(與技術細節分開) |
.Hint("Runbook: https://doc.acme.org/doc/abcd.md") |
為開發者新增除錯提示 |
.Owner("team/slack") |
識別負責團隊/擁有者 |
.User(id, "k", "v") |
新增使用者識別碼和屬性 |
.Tenant(id, "k", "v") |
新增租戶/組織上下文和屬性 |
.Trace(id) |
新增追蹤/關聯 ID(預設:ULID) |
.Span(id) |
新增表示工作/操作單元的跨度 ID(預設:ULID) |
.Time(t) |
覆蓋錯誤時間戳記(預設:time.Now()) |
.Since(t) |
設定基於自 t 以來的持續時間(透過 err.Duration() 暴露) |
.Duration(d) |
設定明確的錯誤持續時間 |
.Request(req, includeBody) |
附加 *http.Request(可選擇包含請求主體) |
.Response(res, includeBody) |
附加 *http.Response(可選擇包含回應主體) |
oops.FromContext(ctx) |
從儲存在 Go context 中的 OopsErrorBuilder 開始 |
常見情境
資料庫/儲存庫層
func (r *UserRepository) FetchUser(id string) (*User, error) {
query := "SELECT * FROM users WHERE id = $1"
row, err := r.db.Query(query, id)
if err != nil {
return nil, oops.
In("user-repository").
Tags("database", "postgres").
With("query", query).
With("user_id", id).
Wrapf(err, "failed to fetch user from database")
}
// ...
}
HTTP 處理層
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
err := h.service.CreateUser(r.Context(), userID)
if err != nil {
err = oops.
In("http-handler").
Tags("endpoint", "/users").
Request(r, false).
User(userID).
Wrapf(err, "create user failed")
http.Error(w, oops.GetPublic(err, "Internal server error"), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
服務層與可重複使用的建構器
func (s *UserService) CreateOrder(ctx context.Context, req CreateOrderRequest) error {
builder := oops.
In("order-service").
Tags("orders", "checkout").
Tenant(req.TenantID, "plan", req.Plan).
User(req.UserID, "email", req.UserEmail)
product, err := s.catalog.GetProduct(ctx, req.ProductID)
if err != nil {
return builder.
With("product_id", req.ProductID).
Wrapf(err, "product lookup failed")
}
if product.Stock < req.Quantity {
return builder.
Code("insufficient_stock").
Public("Not enough items in stock.").
With("requested", req.Quantity).
With("available", product.Stock).
Errorf("insufficient stock for product %s", req.ProductID)
}
return nil
}
錯誤包裝最佳實踐
要:直接包裝,無需 nil 檢查
// ✓ 好 — 如果 err 為 nil,Wrap 回傳 nil
return oops.Wrapf(err, "operation failed")
// ✗ 不好 — 不必要的 nil 檢查
if err != nil {
return oops.Wrapf(err, "operation failed")
}
return nil
要:在每一層添加上下文
每個架構層都應透過 Wrap/Wrapf 添加上下文 — 至少每個套件邊界一次(不一定是每個函式呼叫)。
// ✓ 好 — 每一層添加相關上下文
func Controller() error {
return oops.In("controller").Trace(traceID).Wrapf(Service(), "user request failed")
}
func Service() error {
return oops.In("service").With("op", "create_user").Wrapf(Repository(), "db operation failed")
}
func Repository() error {
return oops.In("repository").Tags("database", "postgres").Errorf("connection timeout")
}
要:保持錯誤訊息低基數
錯誤訊息必須是低基數,以便 APM 聚合。將變數資料插入訊息中會破壞 Datadog、Loki、Sentry 的分組。
// ✗ 不好 — 高基數,破壞 APM 分組
oops.Errorf("failed to process user %s in tenant %s", userID, tenantID)
// ✓ 好 — 靜態訊息 + 結構化屬性
oops.With("user_id", userID).With("tenant_id", tenantID).Errorf("failed to process user")
恐慌恢復
oops.Recover() 必須在 goroutine 邊界使用。將恐慌轉換為結構化錯誤:
func ProcessData(data string) (err error) {
return oops.
In("data-processor").
Code("panic_recovered").
Hint("Check input data format and dependencies").
With("input_data", data).
Recover(func() {
riskyOperation(data)
})
}
存取錯誤資訊
samber/oops 錯誤實作了標準的 error 介面。存取額外資訊:
if oopsErr, ok := err.(oops.OopsError); ok {
fmt.Println("Code:", oopsErr.Code())
fmt.Println("Domain:", oopsErr.Domain())
fmt.Println("Tags:", oopsErr.Tags())
fmt.Println("Context:", oopsErr.Context())
fmt.Println("Stacktrace:", oopsErr.Stacktrace())
}
// 取得公開訊息,附帶備用值
publicMsg := oops.GetPublic(err, "Something went wrong")
輸出格式
fmt.Printf("%+v\n", err) // 詳細輸出,含堆疊追蹤
bytes, _ := json.Marshal(err) // JSON 格式,用於日誌
slog.Error(err.Error(), slog.Any("error", err)) // slog 整合
上下文傳遞
透過 Go context 攜帶錯誤上下文:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
builder := oops.
In("http").
Request(r, false).
Trace(r.Header.Get("X-Trace-ID"))
ctx := oops.WithBuilder(r.Context(), builder)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(ctx context.Context) error {
return oops.FromContext(ctx).Tags("handler", "users").Errorf("something failed")
}
關於斷言、設定和其他日誌範例,請參閱進階模式。
參考資料
交叉參考
- → 請參閱
samber/cc-skills-golang@golang-error-handling技能以了解一般錯誤處理模式 - → 請參閱
samber/cc-skills-golang@golang-observability技能以了解日誌整合和結構化日誌






