Bryan

Bryan

twitter
medium

Go 中 error 序列化的坑

引言#

請猜測:下面的輸出是什麼?

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.Marshalerencoding.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 库,但基於實際需求做了一定的優化:

  1. (相比標準庫)為所有的錯誤都包裝了調用堆疊資訊。
  2. 對於已經存在調用堆疊資訊的,不會覆蓋(來保證永遠可以拿到最深層的調用堆疊資訊)。
  3. 支持在 WithStack 時指定 skip 來使用上層堆疊(用於編寫工具函數)。
  4. 堆疊資訊的 StackTraceFrame 可訪問,以供外部工具(例如日誌處理庫)結構化利用。
  5. 增加 Panic 函數,調用時會自動生成 error 並記錄 panic 位置資訊。
  6. 所有 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 類型的字段。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。