Go 填坑:使用 UnmarshalJSON 接口實現自定義 unmarshal 的坑
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 , 無意間點進去發(fā)現這個問題還挺有意思的,自己經過實踐后才發(fā)現,這應該是 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 結構體,并且?guī)в幸粋€ 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
}
當一切準備就緒的時候,讓我們執(zhí)行代碼。

現象是,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,導致了詭異的錯誤信息。
如何解決
解決這個問題的方式有很多種,這里給出一種比較穩(wěn)妥的思路:將嵌入字段的處理與其余字段分開,代碼如下:
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 中搜了一下,發(fā)現早在 2016 年就有人踩過這個坑了,如今又有人踩到,遂寫下此文,勿再入坑
。

總結
go 沒有繼承,也不要把面向對象的繼承思想直接用到 go 的代碼中,否則會遇到意想不到的 bug ; 結構體嵌入字段的實現方法的執(zhí)行順序要了解 - 從外層到內層。
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術
職場和創(chuàng)業(yè)經驗
Go語言中文網
每天為你
分享 Go 知識
Go愛好者值得關注
