golang 使用 UnmarshalJSON
實現自定義 marshal/unmarshal
的坑git
背景
Go 語言標準庫
encoding/json
提供了操做 JSON 的方法,通常可使用json.Marshal
和json.Unmarshal
來序列化和解析 JSON 字符串。當你想實現自定義的 Unmarshal 方法,就要實現 Unmarshaler 接口。一位老哥在 golang/go 項目下提了一個相似的 issue:https://github.com/golang/go/issues/39470 , 無心間點進去發現這個問題還挺有意思的,本身通過實踐後才發現,這應該是 golang 中的一個大坑。github
先來看一下這位仁兄遇到了什麼問題: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 接口。web
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
}
當一切準備就緒的時候,讓咱們執行代碼。json
現象是,Num
字段並無被解析成功 🤔 。微信
分析問題
代碼看起來並無什麼問題,用迴歸本質的方式解釋起來就是,結構體嵌入並實現接口方法。那先讓咱們來看一段迴歸本質的代碼:app
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 的這個實現就會致使不少意想不到的事情。好比上面這位老哥遇到的詭異事情。this
這個問題的本質和這位老哥遇到的問題同樣,由於 Object
中嵌入了 Nested
,因此有了 UnmarshalJSON, 符合了 json 包中 Unmarshaler 接口,因此內部用接口去處理的時候,Object
是知足的,但實際處理的是 Nested
,也就是以 Nested
做爲實體來進行 UnmarshalJSON,致使了詭異的錯誤信息。url
如何解決
解決這個問題的方式有不少種,這裏給出一種比較穩妥的思路:將嵌入字段的處理與其他字段分開,代碼以下:
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 ; -
結構體嵌入字段的實現方法的執行順序要了解 - 從外層到內層。
本文分享自微信公衆號 - Go Official Blog(Go_Official_Blog)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。