引言#
請猜測:下面的輸出是什麼?
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonStr, _ := json.Marshal(fmt.Errorf("This is an error"))
fmt.Println(string(jsonStr))
}
答案
出乎意料——輸出並非 "This is an error",而是一個 {} !這個問題實際上早有討論,然而並沒有得到官方的答覆(或許是因為需要序列化 error 的地方太少了?)
根源#
Go 中 error 不過是一個介面,一個沒有任何特殊點的介面。
而 Go 中 fmt.Errorf
所返回的 error 類型定義為:
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
// 或者
type wrapError struct {
msg string
err error
}
在 JSON 序列化時,遵循標準的 struct 序列化規則:保留所有的大寫字母開頭的字段而省略其餘字段,並不會去調用底層的 Error 方法來獲取錯誤的資訊。因此,最終結果就是簡單的 {}
。
解決#
閱讀 json 序列化相關的原始碼。
// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
// If we have a non-pointer value whose type implements
// Marshaler with a value receiver, then we're better off taking
// the address of the value - otherwise we end up with an
// allocation as we cast the value to an interface.
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
}
if t.Implements(marshalerType) {
return marshalerEncoder
}
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {
return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false))
}
if t.Implements(textMarshalerType) {
return textMarshalerEncoder
}
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Pointer:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}
可以發現,在進行類型判斷之前,會依次判斷類型是否實現了 json.Marshaler 或 encoding.TextMarshaler 介面,如果實現了則使用其對應的實現。
因此,實現 encoding.TextMarshaler
介面即可解決問題:
func (e MyError) MarshalText() ([]byte, error) {
return []byte(e.Error()), nil
}
並沒有完事!#
上面看似解決了問題,但是卻給我們提供了一個隱性的要求:我們不可以使用任何 Go 標準庫提供的錯誤類型,因為我們無法為其實現 TextMarshaler 介面。
最優的方案實際上是全局使用第三方庫。這裡推薦使用我自己的 ee 錯誤處理庫(github.com/ImSingee/go-ex/ee),其修改自官方的 pkg/errors 库,但基於實際需求做了一定的優化:
- (相比標準庫)為所有的錯誤都包裝了調用堆疊資訊。
- 對於已經存在調用堆疊資訊的,不會覆蓋(來保證永遠可以拿到最深層的調用堆疊資訊)。
- 支持在
WithStack
時指定 skip 來使用上層堆疊(用於編寫工具函數)。 - 堆疊資訊的
StackTrace
和Frame
可訪問,以供外部工具(例如日誌處理庫)結構化利用。 - 增加
Panic
函數,調用時會自動生成 error 並記錄 panic 位置資訊。 - 所有 error 都實現了 TextMarshaler 介面,對序列化友好。
另外,即使包裹了自定義錯誤,總有一些漏網之魚,因此一個建議是在 json 序列化之前將可能為 error 的字段進行判斷來替換。這裡提供一個示例函數,實際使用時可根據需要修改使用:
// Special Check
if err, ok := fields["error"].(error); ok {
_, tmok := err.(encoding.TextMarshaler)
_, jmok := err.(json.Marshaler)
if !tmok && !jmok {
fields["error"] = err.Error()
}
}
總結#
我所有文章最不會寫的就是總結,因此這個總結由 AI 生成😂
本文主要講解了在 Go 語言中如何序列化 error 類型。為了解決 fmt.Errorf () 所返回的 error 類型結構無法符合 JSON 序列化的標準的問題,我們需要實現 encoding.TextMarshaler
介面。同時,本文推薦使用第三方庫 ee,它繼承了官方庫 pkg/errors 的優點,並且實現了所有錯誤類型都實現 TextMarshaler 介面的特性。最後,我們還提供了一個代碼示例,可以幫助你在序列化之前避免判斷是否為 error 類型的字段。