Go 語言中 JSON 處理技巧總結

大家好,我是漁夫子。

本文總結了 go 語言中對 JSON 數據結構和結構體之間相互轉換問題及解決方法。

基礎使用

使用 Go 標準庫中的 json.Marshal()json.Unmarshal進行基本的序列化和反序列化。

type Person struct {
 Name   string
 Age    int64
 Weight float64
}

func main() {
 p1 := Person{
  Name:   "Go學堂",
  Age:    18,
  Weight: 71.5,
 }
 // 將結構體轉換成json串
 b, _ := json.Marshal(p1)

 fmt.Printf("str:%s\n", b)
    
 // 將json串轉換成結構體
 var p2 Person
 json.Unmarshal(b, &p2)

 fmt.Printf("p2:%#v\n", p2)
}

輸出:

str:{"Name":"Go學堂","Age":18,"Weight":71.5}

p2:main.Person{Name:"Go學堂", Age:18, Weight:71.5}

給結構體指定 tag 屬性

Tag是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。 Tag在結構體字段的後方定義,由一對 ** 反引號 ****``**包裹起來,具體的格式如 Name 字段:

type Person struct {
 Name   string `json:"name"`
 Age    int64
 Weight float64
}

這裏的json:"name"就是給 Name 字段的設置的 tag。

tag 由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。同一個結構體字段可以設置多個鍵值對 tag,不同的鍵值對之間使用空格分隔。如下:

type Person struct {
 Name   string `json:"name" param:"name"`
 Age    int64
 Weight float64
}

使用 json tag 指定字段名

序列化與反序列化默認情況下使用結構體的字段名,我們可以通過給結構體字段添加 tag 來指定 json 序列化生成的字段名

// 使用json tag指定序列化與反序列化時的行爲
type Person struct {
 Name   string `json:"name"` // 指定json序列化/反序列化時使用小寫name
 Age    int64
 Weight float64
}

忽略某個字段

如果你想在 json 序列化 / 反序列化的時候忽略掉結構體中的某個字段,可以按如下方式在 tag 中添加**-**。如下Person中的Weight字段

// 使用json tag指定json序列化與反序列化時的行爲
type Person struct {
 Name   string `json:"name"` // 指定json序列化/反序列化時使用小寫name
 Age    int64
 Weight float64 `json:"-"` // 指定json序列化/反序列化時忽略此字段
}

忽略空值字段

當 struct 中的字段沒有值時, json.Marshal() 序列化的時候不會忽略這些字段,而是默認輸出字段的類型零值(例如 int 和 float 類型零值是 0,string 類型零值是 "",對象類型零值是 nil)。

如果想要在序列序列化時忽略這些沒有值的字段時,可以在對應字段添加omitemptytag。

將空值輸出的例子

下面是將EmailHobby字段的空值輸出的例子:

type User struct {
 Name  string   `json:"name"`
 Email string   `json:"email"`
 Hobby []string `json:"hobby"`
}

func omitemptyDemo() {
 u1 := User{
  Name: "Go學堂",
 }
 // struct -> json string
 b, _ := json.Marshal(u1)

 fmt.Printf("str:%s\n", b)
}

輸出結果:

str:{"name":"Go學堂","email":"","hobby":null}

將空值忽略的例子

如果想要在最終的序列化結果中去掉空值字段,可以像下面這樣定義結構體,在 Email 和 Hobby 的 tag 中添加omitempty,以表示若字段值爲零值,則在序列化時忽略該字段

// 在tag中添加omitempty忽略空值
// 注意這裏 hobby,omitempty 合起來是json tag值,中間用英文逗號分隔
type User struct {
 Name  string   `json:"name"`
 Email string   `json:"email,omitempty"`
 Hobby []string `json:"hobby,omitempty"`
}

此時,再執行上述的程序,輸出結果如下:

str:{"name":"Go學堂"} // 序列化結果中沒有email和hobby字段

忽略嵌套結構體空值字段

結構體嵌套可分匿名結構體嵌套具名嵌套。這兩種方式在進行 json 序列化時的行爲會有所不同。下面通過示例來說明。

匿名嵌套

匿名嵌套是指在結構體中不指定字段名,只指定類型的字段。匿名嵌套在 json 序列化時,會直接輸出類型對應的字段。如下

type User struct {
 Name  string   `json:"name"`
 Email string   `json:"email,omitempty"`
 Hobby []string `json:"hobby,omitempty"`
 Profile
}

type Profile struct {
 Website string `json:"site"`
 Slogan  string `json:"slogan"`
}

func nestedStructDemo() {
 u1 := User{
  Name:  "Go學堂",
  Hobby: []string{"golang""rust"},
 }
 b, _ := json.Marshal(u1)

 fmt.Printf("str:%s\n", b)
}

匿名嵌套 Profile 時序列化後的 json 串爲單層的:

str:{"name":"Go學堂","hobby":["golang","rust"],"site":"","slogan":""}

具名嵌套

想要變成嵌套的 json 串,需要改爲具名嵌套或定義字段 tag

type User struct {
 Name    string   `json:"name"`
 Email   string   `json:"email,omitempty"`
 Hobby   []string `json:"hobby,omitempty"`
 Profile `json:"profile"`
}
// str:{"name":"Go學堂","hobby":["golang","rust"],"profile":{"site":"","slogan":""}}

想要在嵌套的結構體爲空值時,忽略該字段,僅添加omitempty是不夠的

type User struct {
 Name     string   `json:"name"`
 Email    string   `json:"email,omitempty"`
 Hobby    []string `json:"hobby,omitempty"`
 Profile `json:"profile,omitempty"`
}
// str:{"name":"Go學堂","hobby":["golang","rust"],"profile":{"site":"","slogan":""}}

還需要使用嵌套的結構體指針

type User struct {
 Name     string   `json:"name"`
 Email    string   `json:"email,omitempty"`
 Hobby    []string `json:"hobby,omitempty"`
 *Profile `json:"profile,omitempty"`
}
// str:{"name":"Go學堂","hobby":["golang","rust"]}

不修改原結構體,忽略空值字段

我們需要 json 序列化User,但是不想把密碼也序列化,又不想修改 User 結構體,這個時候我們就可以使用創建另外一個結構體 PublicUser 匿名嵌套原 User同時指定 Password 字段爲匿名結構體指針類型,並添加omitemptytag,示例代碼如下:

type User struct {
 Name     string `json:"name"`
 Password string `json:"password"`
}

type PublicUser struct {
 *User             // 匿名嵌套
 Password *struct{} `json:"password,omitempty"`
}

func omitPasswordDemo() {
 u1 := User{
  Name:     "Go學堂",
  Password: "123456",
 }
 b, _ := json.Marshal(PublicUser{User: &u1})

 fmt.Printf("str:%s\n", b)  // str:{"name":"Go學堂"}
}

優雅處理字符串格式的數字

有時候,前端在傳遞來的 json 數據中可能會使用字符串類型的數字,這個時候可以在結構體 tag 中添加 string 來告訴 json 包從字符串中解析相應字段的數據

type Card struct {
 ID    int64   `json:"id,string"`    // 添加string tag
 Score float64 `json:"score,string"` // 添加string tag
}

func intAndStringDemo() {
 jsonStr1 := `{"id""1234567","score""88.50"}`
 var c1 Card
 if err := json.Unmarshal([]byte(jsonStr1)&c1); err != nil {
  fmt.Printf("json.Unmarsha jsonStr1 failed, err:%v\n", err)
  return
 }
 fmt.Printf("c1:%#v\n", c1) // c1:main.Card{ID:1234567, Score:88.5}
}

整數變浮點數

在 JSON 協議中是沒有整型和浮點型之分的,它們統稱爲 number。json 字符串中的數字經過 Go 語言中的 json 包反序列化之後都會成爲 float64 類型。下面的代碼便演示了這個問題:

func jsonDemo() {
 // map[string]interface{} -> json string
 var m = make(map[string]interface{}, 1)
 m["count"] = 1 // int
 b, _ := json.Marshal(m)

 fmt.Printf("str:%#v\n", string(b))
    
 // json string -> map[string]interface{}
 var m2 map[string]interface{}
 json.Unmarshal(b, &m2)

 fmt.Printf("value:%v\n", m2["count"]) // 1
 fmt.Printf("type:%T\n", m2["count"])  // float64
}

你看,原本 m["count"] 的值是整型 1,但經過序列化和再反序列化後就變成了float64類型了。

這種場景下如果想更合理的處理數字就需要使用decoder去反序列化,示例代碼如下:

func decoderDemo() {
 // map[string]interface{} -> json string
 var m = make(map[string]interface{}, 1)
 m["count"] = 1 // int
 b, _ := json.Marshal(m)

 fmt.Printf("str:%#v\n", string(b))
 // json string -> map[string]interface{}
 var m2 map[string]interface{}
 // 使用decoder方式反序列化,指定使用number類型
 decoder := json.NewDecoder(bytes.NewReader(b))
 decoder.UseNumber()
 decoder.Decode(&m2)

 fmt.Printf("value:%v\n", m2["count"]) // 1
 fmt.Printf("type:%T\n", m2["count"])  // json.Number
    
 // 將m2["count"]轉爲json.Number之後調用Int64()方法獲得int64類型的值
 count, _ := m2["count"].(json.Number).Int64()

 fmt.Printf("type:%T\n", int(count)) // int
}

json.Number的源碼定義如下:

// A Number represents a JSON number literal.
type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
 return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
 return strconv.ParseInt(string(n), 10, 64)
}

我們在處理number類型的 json 字段時需要先得到json.Number類型,然後根據該字段的實際類型調用Float64()Int64()

自定義解析時間字段

Go 語言內置的 json 包使用 RFC3339 標準中定義的時間格式,對我們序列化時間字段的時候有很多限制。

type Post struct {
 CreateTime time.Time `json:"create_time"`
}

func timeFieldDemo() {
 p1 := Post{CreateTime: time.Now()}
 b, _ := json.Marshal(p1) //這裏會輸出RFC3339格式的時間

 fmt.Printf("str:%s\n", b)
 jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
 var p2 Post
    //  反序列化時會報錯
 if err := json.Unmarshal([]byte(jsonStr)&p2); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%v\n", err)
  return
 }
 fmt.Printf("p2:%#v\n", p2)
}

上面的代碼輸出結果如下:

str:{"create_time":"2023-06-01T09:28:06.799214+08:00"}
json.Unmarshal failed, err:parsing time ""2023-06-01 09:28:06"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"

也就是內置的 json 包不識別我們常用的字符串時間格式,如2023-06-01 12:25:42。 不過我們通過實現 json.Marshaler/json.Unmarshaler 接口來實現自定義的事件格式解析。 如下,CustomTime 類型實現了 json 的接口。

type CustomTime struct {
 time.Time
}

const ctLayout = "2006-01-02 15:04:05"

var nilTime = (time.Time{}).UnixNano()

// 實現了json.Unmarshaler接口中的方法
func (ct *CustomTime) UnmarshalJSON([]byte) (err error) {
 s := strings.Trim(string(b)"\"")
 if s == "null" {
  ct.Time = time.Time{}
  return
 }
 ct.Time, err = time.Parse(ctLayout, s)
 return
}

// 實現了json.Marshaler接口中的方法
func (ct *CustomTime) MarshalJSON() ([]byte, error) {
 if ct.Time.UnixNano() == nilTime {
  return []byte("null"), nil
 }
 return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(ctLayout))), nil
}

func (ct *CustomTime) IsSet() bool {
 return ct.UnixNano() != nilTime
}

type Post struct {
 CreateTime CustomTime `json:"create_time"`
}

func timeFieldDemo() {
 p1 := Post{CreateTime: CustomTime{time.Now()}}
 b, err := json.Marshal(p1)
 if err != nil {
  fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
  return
 }
 fmt.Printf("str:%s\n", b)
 jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
 var p2 Post
 if err := json.Unmarshal([]byte(jsonStr)&p2); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%v\n", err)
  return
 }
 fmt.Printf("p2:%#v\n", p2)
}

自定義 MarshalJSON 和 UnmarshalJSON 方法

上面那種自定義類型的方法稍顯囉嗦了一點,下面來看一種相對便捷的方法。 首先你需要知道的是,如果你能夠爲某個類型實現了MarshalJSON()([]byte, error)UnmarshalJSON(b []byte) error方法,那麼這個類型在序列化(MarshalJSON)/ 反序列化(UnmarshalJSON)時就會使用你定製的相應方法。

type Order struct {
 ID          int       `json:"id"`
 Title       string    `json:"title"`
 CreatedTime time.Time `json:"created_time"`
}

const layout = "2006-01-02 15:04:05"

// MarshalJSON 爲Order類型實現自定義的MarshalJSON方法
func (o *Order) MarshalJSON() ([]byte, error) {
 type TempOrder Order // 定義與Order字段一致的新類型
 return json.Marshal(struct {
  CreatedTime string `json:"created_time"`
  *TempOrder         // 避免直接嵌套Order進入死循環
 }{
  CreatedTime: o.CreatedTime.Format(layout),
  TempOrder:   (*TempOrder)(o),
 })
}

// UnmarshalJSON 爲Order類型實現自定義的UnmarshalJSON方法
func (o *Order) UnmarshalJSON(data []byte) error {
 type TempOrder Order // 定義與Order字段一致的新類型
 ot := struct {
  CreatedTime string `json:"created_time"`
  *TempOrder         // 避免直接嵌套Order進入死循環
 }{
  TempOrder: (*TempOrder)(o),
 }
 if err := json.Unmarshal(data, &ot); err != nil {
  return err
 }
 var err error
 o.CreatedTime, err = time.Parse(layout, ot.CreatedTime)
 if err != nil {
  return err
 }
 return nil
}

// 自定義序列化方法
func customMethodDemo() {
 o1 := Order{
  ID:          123456,
  Title:       "《Go學堂的Golang學習之旅》",
  CreatedTime: time.Now(),
 }
 // 通過自定義的MarshalJSON方法實現struct -> json string
 b, _ := json.Marshal(&o1)

 fmt.Printf("str:%s\n", b)
 // 通過自定義的UnmarshalJSON方法實現json string -> struct
 jsonStr := `{"created_time":"2020-04-05 10:18:20","id":123456,"title":"《Go學堂的Golang學習之旅》"}`
 var o2 Order
 if err := json.Unmarshal([]byte(jsonStr)&o2); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%v\n", err)
  return
 }
 fmt.Printf("o2:%#v\n", o2)
}

輸出結果:

str:{"created_time":"2020-04-05 10:32:20","id":123456,"title":"《Go學堂的Golang學習之旅》"}
o2:main.Order{ID:123456, Title:"《Go學堂的Golang學習之旅》", CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}}

使用匿名結構體添加字段

使用內嵌結構體能夠擴展結構體的字段,但有時候我們沒有必要單獨定義新的結構體,可以使用匿名結構體簡化操作

type UserInfo struct {
 ID   int    `json:"id"`
 Name string `json:"name"`
}

func anonymousStructDemo() {
 u1 := UserInfo{
  ID:   123456,
  Name: "Go學堂",
 }
 // 使用匿名結構體內嵌User並添加額外字段Token
 b, err := json.Marshal(struct {
  *UserInfo
  Token string `json:"token"`
 }{
  &u1,
  "91je3a4s72d1da96h",
 })
 if err != nil {
  fmt.Printf("json.Marsha failed, err:%v\n", err)
  return
 }
 fmt.Printf("str:%s\n", b)
 // str:{"id":123456,"name":"Go學堂","token":"91je3a4s72d1da96h"}
}

使用匿名結構體組合多個結構體

同理,也可以使用匿名結構體來組合多個結構體來序列化與反序列化數據:

type Comment struct {
 Content string
}

type Image struct {
 Title string `json:"title"`
 URL   string `json:"url"`
}

func anonymousStructDemo2() {
 c1 := Comment{
  Content: "來學編程呀",
 }
 i1 := Image{
  Title: "Go學堂",
  URL:   "https://goxuetang.github.io",
 }
 // struct -> json string
 b, _ := json.Marshal(struct {
  *Comment
  *Image
 }{&c1, &i1})

 fmt.Printf("str:%s\n", b)
 // json string -> struct
 jsonStr := `{"Content":"來學編程呀","title":"Go學堂","url":"https://goxuetang.github.io"}`
 var (
  c2 Comment
  i2 Image
 )
 if err := json.Unmarshal([]byte(jsonStr)&struct {
  *Comment
  *Image
 }{&c2, &i2}); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%v\n", err)
  return
 }
 fmt.Printf("c2:%#v i2:%#v\n", c2, i2)
}

輸出:

str:{"Content":"來學編程呀","title":"Go學堂","url":"https://goxuetang.github.io"}
c2:main.Comment{Content:"來學編程呀"} i2:main.Image{Title:"Go學堂", URL:"https://goxuetang.github.io"}

處理不確定層級的 json

如果 json 串沒有固定的格式導致不好定義與其相對應的結構體時,我們可以使用json.RawMessage原始字節數據保存下來。

type sendMsg struct {
 User string `json:"user"`
 Msg  string `json:"msg"`
}

func rawMessageDemo() {
 jsonStr := `{"sendMsg":{"user":"Go學堂","msg":"來學編程呀"},"say":"Hello"}`
 // 定義一個map,value類型爲json.RawMessage,方便後續更靈活地處理
 var data map[string]json.RawMessage
 if err := json.Unmarshal([]byte(jsonStr)&data); err != nil {
  fmt.Printf("json.Unmarshal jsonStr failed, err:%v\n", err)
  return
 }
 var msg sendMsg
 if err := json.Unmarshal(data["sendMsg"]&msg); err != nil {
  fmt.Printf("json.Unmarshal failed, err:%v\n", err)
  return
 }
 fmt.Printf("msg:%#v\n", msg)
 // msg:main.sendMsg{User:"Go學堂", Msg:"來學編程呀"}
}

總結

本文總結了 Go 語言在結構體和 json 串之間相互之間進行轉換時的一些技巧。同時,這些技巧也是研發者在實際項目中需要注意的地方,希望本文對你有所幫助。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/js3m_Fe6k4ys4aBSFDJhLQ