Go 填坑:使用 UnmarshalJSON 接口实现自定义 unmarshal 的坑
共 4924字,需浏览 10分钟
·
2020-08-26 13:09
golang 使用 UnmarshalJSON
实现自定义 marshal/unmarshal
的坑
背景
Go 语言标准库
encoding/json
提供了操作 JSON 的方法,一般可以使用json.Marshal
和json.Unmarshal
来序列化和解析 JSON 字符串。当你想实现自定义的 Unmarshal 方法,就要实现 Unmarshaler 接口。一位老哥在 golang/go 项目下提了一个类似的 issue:https://github.com/golang/go/issues/39470 , 无意间点进去发现这个问题还挺有意思的,自己经过实践后才发现,这应该是 golang 中的一个大坑。
先来看一下这位仁兄遇到了什么问题:
package main
import (
"encoding/json"
"fmt"
"time"
)
var testJSON = `{"num":5,"duration":"5s"}`
type Nested struct {
Dur time.Duration `json:"duration"`
}
func (n *Nested) UnmarshalJSON(data []byte) error {
*n = Nested{}
tmp := struct {
Dur string `json:"duration"`
}{}
fmt.Printf("parsing nested json %s \n", string(data))
if err := json.Unmarshal(data, &tmp); err != nil {
fmt.Printf("failed to parse nested: %v", err)
return err
}
tmpDur, err := time.ParseDuration(tmp.Dur)
if err != nil {
fmt.Printf("failed to parse duration: %v", err)
return err
}
(*n).Dur = tmpDur
return nil
}
type Object struct {
Nested
Num int `json:"num"`
}
//uncommenting this method still doesnt help.
//tmp is parsed with the completed json at Nested
//which doesnt take care of Num field, so Num is zero value.
func (o *Object) UnmarshalJSON(data []byte) error {
*o = Object{}
tmp := struct {
Nested
Num int `json:"num"`
}{}
fmt.Printf("parsing object json %s \n", string(data))
if err := json.Unmarshal(data, &tmp); err != nil {
fmt.Printf("failed to parse object: %v", err)
return err
}
fmt.Printf("tmp object: %+v \n", tmp)
(*o).Num = tmp.Num
(*o).Nested = tmp.Nested
return nil
}
func main() {
obj := Object{}
if err := json.Unmarshal([]byte(testJSON), &obj); err != nil {
fmt.Printf("failed to parse result: %v", err)
return
}
fmt.Printf("result: %+v \n", obj)
}
代码看起来是要实现一个带有自定义功能的 unmarshal
,Object
结构体内嵌了 Nested
结构体,并且带有一个 Num
字段,想要把 json string {"num":5,"duration":"5s"}
unmarshal 到结构体 Object
中。代码看上去没什么问题,Object
中嵌入了 Nested
,都实现了 UnmarshalJSON, 符合了 json 包中 Unmarshaler 接口。
package json
..........
/ By convention, to approximate the behavior of Unmarshal itself,
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
当一切准备就绪的时候,让我们执行代码。
现象是,Num
字段并没有被解析成功 ? 。
分析问题
代码看起来并没有什么问题,用回归本质的方式解释起来就是,结构体嵌入并实现接口方法。那先让我们来看一段回归本质的代码:
package main
import "fmt"
type Funer interface{
Name()string
PrintName()
}
type A struct {
}
func (a *A) Name() string {
return "a"
}
func (a *A) PrintName() {
fmt.Println(a.Name())
}
type B struct {
A
}
func (b *B) Name() string {
return "b"
}
func getBer() Funer {
return &B{}
}
func main() {
b := getBer()
b.PrintName()
}
这段代码的输出应该是什么?考虑 20s 说出你的答案。
这个实现中,正确的输出的是 a
,而通常在 C++,Java,Python 中这种思想下,我们给出的答案往往是 b
,受到之前的语言思维习惯影响,那么 go 的这个实现就会导致很多意想不到的事情。比如上面这位老哥遇到的诡异事情。
这个问题的本质和这位老哥遇到的问题一样,因为 Object
中嵌入了 Nested
,所以有了 UnmarshalJSON, 符合了 json 包中 Unmarshaler 接口,所以内部用接口去处理的时候,Object
是满足的,但实际处理的是 Nested
,也就是以 Nested
作为实体来进行 UnmarshalJSON,导致了诡异的错误信息。
如何解决
解决这个问题的方式有很多种,这里给出一种比较稳妥的思路:将嵌入字段的处理与其余字段分开,代码如下:
package main
import (
"encoding/json"
"fmt"
"time"
)
var testJSON = `{"num":5,"duration":"5s"}`
type Nested struct {
Dur time.Duration `json:"duration"`
}
func (n *Nested) UnmarshalJSON(data []byte) error {
*n = Nested{}
tmp := struct {
Dur string `json:"duration"`
}{}
fmt.Printf("parsing nested json %s \n", string(data))
if err := json.Unmarshal(data, &tmp); err != nil {
fmt.Printf("failed to parse nested: %v", err)
return err
}
tmpDur, err := time.ParseDuration(tmp.Dur)
if err != nil {
fmt.Printf("failed to parse duration: %v", err)
return err
}
(*n).Dur = tmpDur
fmt.Printf("tmp object: %+v \n", tmp)
return nil
}
type Object struct {
Nested
Num int `json:"num"`
}
//uncommenting this method still doesnt help.
//tmp is parsed with the completed json at Nested
//which doesnt take care of Num field, so Num is zero value.
func (o *Object) UnmarshalJSON(data []byte) error {
tmp := struct {
//Nested
Num int `json:"num"`
}{}
// unmarshal Nested alone
tmpNest := struct {
Nested
}{}
fmt.Printf("parsing object json %s \n", string(data))
if err := json.Unmarshal(data, &tmp); err != nil {
fmt.Printf("failed to parse object: %v", err)
return err
}
// the Nested impl UnmarshalJSON, so it should be unmarshaled alone
if err := json.Unmarshal(data, &tmpNest); err != nil {
fmt.Printf("failed to parse object: %v", err)
return err
}
fmt.Printf("tmp object: %+v \n", tmp)
(o).Num = tmp.Num
(o).Nested = tmpNest.Nested
return nil
}
func main() {
obj := Object{}
if err := json.Unmarshal([]byte(testJSON), &obj); err != nil {
fmt.Printf("failed to parse result: %v", err)
return
}
fmt.Printf("result: %+v \n", obj)
}
这样就可以得到正确的自定义解析了。
ps: 笔者在 golang/go
的 issue
中搜了一下,发现早在 2016 年就有人踩过这个坑了,如今又有人踩到,遂写下此文,勿再入坑。
总结
go 没有继承,也不要把面向对象的继承思想直接用到 go 的代码中,否则会遇到意想不到的 bug ; 结构体嵌入字段的实现方法的执行顺序要了解 - 从外层到内层。
推荐阅读
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注