{
"title": "Go 中 error 序列化的坑",
"tags": [
"post",
"Go"
],
"summary": "猜猜在 Go 中如果将 error 类型进行 JSON 序列化会发生什么?",
"sources": [
"xlog"
],
"external_urls": [
"https://articles.singee.me/Go-zhong-error-xu-lie-hua-de-keng"
],
"date_published": "2023-04-06T11:54:14.778Z",
"content": "## 引言\n\n请猜测:下面的输出是什么?\n\n```go\npackage main\n\nimport (\n \"encoding/json\"\n \"fmt\"\n)\n\nfunc main() {\n jsonStr, _ := json.Marshal(fmt.Errorf(\"This is an error\"))\n fmt.Println(string(jsonStr))\n}\n```\n\n\n<details> <summary>答案</summary>\n出乎意料——输出并非 \"This is an error\",而是一个 {} ! \n</details>\n\n \n \n\n这个问题实际上[早有讨论](https://github.com/golang/go/issues/5161),然而并没有得到官方的答复(或许是因为需要序列化 error 的地方太少了?)\n\n## 根源\n\nGo 中 error 不过是一个接口,一个没有任何特殊点的接口。\n\n而 Go 中 `fmt.Errorf` 所返回的 error 类型定义为:\n\n```go\n// errorString is a trivial implementation of error.\ntype errorString struct {\n\ts string\n}\n\n// 或者\n\ntype wrapError struct {\n\tmsg string\n\terr error\n}\n\n```\n\n在 JSON 序列化时,遵循标准的 struct 序列化规则:保留所有的大写字母开头的字段而省略其余字段,并不会去调用底层的 Error 方法来获取错误的信息。因此,最终结果就是简单的 `{}`。\n\n## 解决\n\n阅读 [json 序列化相关的源码](https://github.com/golang/go/blob/go1.20.3/src/encoding/json/encode.go#L415-L461)。\n\n```go\n// newTypeEncoder constructs an encoderFunc for a type.\n// The returned encoder only checks CanAddr when allowAddr is true.\nfunc newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {\n\t// If we have a non-pointer value whose type implements\n\t// Marshaler with a value receiver, then we're better off taking\n\t// the address of the value - otherwise we end up with an\n\t// allocation as we cast the value to an interface.\n\tif t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {\n\t\treturn newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))\n\t}\n\tif t.Implements(marshalerType) {\n\t\treturn marshalerEncoder\n\t}\n\tif t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {\n\t\treturn newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false))\n\t}\n\tif t.Implements(textMarshalerType) {\n\t\treturn textMarshalerEncoder\n\t}\n\n\tswitch t.Kind() {\n\tcase reflect.Bool:\n\t\treturn boolEncoder\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn intEncoder\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\treturn uintEncoder\n\tcase reflect.Float32:\n\t\treturn float32Encoder\n\tcase reflect.Float64:\n\t\treturn float64Encoder\n\tcase reflect.String:\n\t\treturn stringEncoder\n\tcase reflect.Interface:\n\t\treturn interfaceEncoder\n\tcase reflect.Struct:\n\t\treturn newStructEncoder(t)\n\tcase reflect.Map:\n\t\treturn newMapEncoder(t)\n\tcase reflect.Slice:\n\t\treturn newSliceEncoder(t)\n\tcase reflect.Array:\n\t\treturn newArrayEncoder(t)\n\tcase reflect.Pointer:\n\t\treturn newPtrEncoder(t)\n\tdefault:\n\t\treturn unsupportedTypeEncoder\n\t}\n}\n\n```\n\n可以发现,在进行类型判断之前,会依次判断类型是否实现了 [json.Marshaler](https://pkg.go.dev/encoding/json#Marshaler) 或 [encoding.TextMarshaler](https://pkg.go.dev/encoding#TextMarshaler) 接口,如果实现了则使用其对应的实现。\n\n因此,实现 `encoding.TextMarshaler` 接口即可解决问题:\n\n```go\nfunc (e MyError) MarshalText() ([]byte, error) {\n\treturn []byte(e.Error()), nil\n}\n\n```\n\n## 并没有完事!\n\n上面看似解决了问题,但是却给我们提供了一个隐性的要求:我们不可以使用任何 Go 标准库提供的错误类型,因为我们无法为其实现 TextMarshaler 接口。\n\n最优的方案实际上是全局使用第三方库。这里推荐使用我自己的 [ee 错误处理库(github.com/ImSingee/go-ex/ee)](https://pkg.go.dev/github.com/ImSingee/go-ex/ee),其修改自官方的 [pkg/errors](https://pkg.go.dev/github.com/pkg/errors) 库,但基于实际需求做了一定的优化:\n\n1. (相比标准库)为所有的错误都包装了调用栈信息。\n2. 对于已经存在调用栈信息的,不会覆盖(来保证永远可以拿到最深层的调用栈信息)。\n3. 支持在 `WithStack` 时指定 skip 来使用上层栈(用于编写工具函数)。\n4. 栈信息的 `StackTrace` 和 `Frame` 可访问,以供外部工具(例如日志处理库)结构化利用。\n5. 增加 `Panic` 函数,调用时会自动生成 error 并记录 panic 位置信息。\n6. 所有 error 都实现了 TextMarshaler 接口,对序列化友好。\n\n另外,即使包裹了自定义错误,总有一些漏网之鱼,因此一个建议是在 json 序列化之前将可能为 error 的字段进行判断来替换。这里提供一个示例函数,实际使用时可根据需要修改使用:\n\n```go\n// Special Check\nif err, ok := fields[\"error\"].(error); ok {\n\t_, tmok := err.(encoding.TextMarshaler)\n\t_, jmok := err.(json.Marshaler)\n\n\tif !tmok && !jmok {\n\t\tfields[\"error\"] = err.Error()\n\t}\n}\n\n```\n\n## 总结\n\n> 我所有文章最不会写的就是总结,因此这个总结由 AI 生成😂\n\n本文主要讲解了在 Go 语言中如何序列化 error 类型。为了解决 fmt.Errorf() 所返回的 error 类型结构无法符合 JSON 序列化的标准的问题,我们需要实现 `encoding.TextMarshaler` 接口。同时,本文推荐使用第三方库 ee,它继承了官方库 pkg/errors 的优点,并且实现了所有错误类型都实现 TextMarshaler 接口的特性。最后,我们还提供了一个代码示例,可以帮助你在序列化之前避免判断是否为 error 类型的字段。",
"attributes": [
{
"value": "Go-zhong-error-xu-lie-hua-de-keng",
"trait_type": "xlog_slug"
}
]
}