golang 使用 UnmarshalJSON 接口實現自定義 unmarshal 的坑

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

代碼看起來是要實現一個帶有自定義功能的 unmarshalObject 結構體內嵌了 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 年就有人踩過這個坑了,現在又有人踩到,遂寫下此文,勿再入坑

總結

  1. go 沒有繼承,也不要把面向對象的繼承思想直接用到 go 的代碼中,不然會遇到意想不到的 bug ;
  2. 結構體嵌入字段的實現方法的執行順序要了解 - 從外層到內層。


本文分享自微信公衆號 - Go Official Blog(Go_Official_Blog)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索