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 类型的字段。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。