Bryan

Bryan

twitter
medium

Go 中のエラーシリアライゼーションの落とし穴

イントロダクション#

推測してみてください:以下の出力は何でしょうか?

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" ではなく、{} です!

この問題は実際に議論されていますが、公式の回答は得られていません(おそらくエラーをシリアライズする必要がある場所が少ないためでしょうか?)

根本原因#

Go では、エラーは単なるインターフェースであり、特別な点はありません。

そして、Go のfmt.Errorfが返すエラータイプは次のように定義されています:

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

// または

type wrapError struct {
	msg string
	err error
}

JSON シリアライズでは、標準の構造体シリアライズルールに従います:大文字で始まるフィールドはすべて保持し、それ以外のフィールドは省略され、基礎の 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
}

まだ終わりではありません!#

上記の解決策は問題を解決するように見えますが、隠れた要件を提供しています:標準ライブラリで提供されているエラータイプを使用することはできません。なぜなら、それらに対して TextMarshaler インターフェースを実装することができないからです。

最適な解決策は、サードパーティのライブラリをグローバルに使用することです。ここでは、私自身のee エラーハンドリングライブラリ(github.com/ImSingee/go-ex/ee)を推奨します。これは公式のpkg/errorsライブラリをベースにしており、実際の要件に基づいて最適化されています:

  1. (標準ライブラリと比較して)すべてのエラーにスタックトレース情報を追加しました。
  2. スタックトレース情報が既に存在する場合は上書きしません(常に最も深いレベルのスタックトレース情報を取得できるようにするため)。
  3. WithStack時にスキップを指定して上位スタックを使用できるようにサポートします(ユーティリティ関数の作成に使用)。
  4. スタックトレースのStackTraceFrameにアクセスできるようになりました(ログ処理ライブラリなどの外部ツールで構造化利用できます)。
  5. Panic関数を追加し、呼び出されると自動的にエラーを生成し、パニックの位置情報を記録します。
  6. すべてのエラーは TextMarshaler インターフェースを実装しており、シリアライズに対応しています。

さらに、カスタムエラーをラップしても、いくつかのエラーが見逃される可能性があるため、シリアライズする前にエラーの可能性のあるフィールドをチェックして置換することをお勧めします。以下は、使用時に必要に応じて変更できるサンプル関数の例です:

// 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 言語でエラータイプをシリアライズする方法について説明しました。fmt.Errorf()が返すエラータイプの構造が JSON シリアライズの規格に合わない問題を解決するために、encoding.TextMarshalerインターフェースを実装する必要があります。また、この記事では、ee というサードパーティライブラリを使用することをお勧めしました。これは、公式の pkg/errors ライブラリの利点を継承し、すべてのエラータイプが TextMarshaler インターフェースを実装する特徴を持っています。最後に、シリアライズする前にエラータイプのフィールドをチェックして置換するためのコードサンプルを提供しました。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。