本文首发于 xlog,可跳转获得更佳阅读体验

引言

请猜测:下面的输出是什么?

1
2
3
4
5
6
7
8
9
10
11
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 类型定义为:

1
2
3
4
5
6
7
8
9
10
11
12
// errorString is a trivial implementation of error.
type errorString struct {
s string
}

// 或者

type wrapError struct {
msg string
err error
}

在 JSON 序列化时,遵循标准的 struct 序列化规则:保留所有的大写字母开头的字段而省略其余字段,并不会去调用底层的 Error 方法来获取错误的信息。因此,最终结果就是简单的 {}

解决

阅读 json 序列化相关的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 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 接口即可解决问题:

1
2
3
4
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 的字段进行判断来替换。这里提供一个示例函数,实际使用时可根据需要修改使用:

1
2
3
4
5
6
7
8
9
10
// 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 类型的字段。