イントロダクション#
推測してみてください:以下の出力は何でしょうか?
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ライブラリをベースにしており、実際の要件に基づいて最適化されています:
- (標準ライブラリと比較して)すべてのエラーにスタックトレース情報を追加しました。
- スタックトレース情報が既に存在する場合は上書きしません(常に最も深いレベルのスタックトレース情報を取得できるようにするため)。
WithStack
時にスキップを指定して上位スタックを使用できるようにサポートします(ユーティリティ関数の作成に使用)。- スタックトレースの
StackTrace
とFrame
にアクセスできるようになりました(ログ処理ライブラリなどの外部ツールで構造化利用できます)。 Panic
関数を追加し、呼び出されると自動的にエラーを生成し、パニックの位置情報を記録します。- すべてのエラーは 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 インターフェースを実装する特徴を持っています。最後に、シリアライズする前にエラータイプのフィールドをチェックして置換するためのコードサンプルを提供しました。