golang 使用 UnmarshalJSON 实现自定义 marshal/unmarshal 的坑

8,598 阅读4分钟

golang 使用 UnmarshalJSON 实现自定义 marshal/unmarshal 的坑

背景

一位老哥在 golang/go 项目下提了一个 issue:github.com/golang/go/i… , 无意间点进去发现这个问题还挺有意思的,自己在实践后才发现,这应该是 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)
}

这样就可以得到正确的自定义解析了。

总结

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


                                    **官方资讯/最新技术/独家解读**