Go 填坑:使用 UnmarshalJSON 接口实现自定义 unmarshal 的坑

Go语言精选

共 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)
}

代码看起来是要实现一个带有自定义功能的 unmarshalObject 结构体内嵌了 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 年就有人踩过这个坑了,如今又有人踩到,遂写下此文,勿再入坑

总结

  1. go 没有继承,也不要把面向对象的继承思想直接用到 go 的代码中,否则会遇到意想不到的 bug ;
  2. 结构体嵌入字段的实现方法的执行顺序要了解 - 从外层到内层。





推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注



浏览 80
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报