title: 【轉載】你需要知道的那些 go 語言 json 技巧
date: 2021-08-09 16:33:33
comment: false
toc: true
category:
- Golang
tags: - 轉載
- Go
- Json
本文轉載自:你需要知道的那些 go 語言 json 技巧 | 李文周的博客
本文總結了我平時在項目中遇到的那些關於 go 語言 JSON 數據與結構體之間相互轉換的問題及解決辦法。
基本的序列化#
首先我們來看一下 Go 語言中json.Marshal()
(序列化)與json.Unmarshal
(反序列化)的基本用法。
type Person struct {
Name string
Age int64
Weight float64
}
func main() {
p1 := Person{
Name: "七米",
Age: 18,
Weight: 71.5,
}
// struct -> json string
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
var p2 Person
err = json.Unmarshal(b, &p2)
if err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
輸出:
str:{"Name":"七米","Age":18,"Weight":71.5}
p2:main.Person{Name:"七米", Age:18, Weight:71.5}
結構體 tag 介紹#
Tag
是結構體的元信息,可以在運行的時候通過反射的機制讀取出來。 Tag
在結構體字段的後方定義,由一對反引號 包裹起來,具體的格式如下:
`key1:"value1" key2:"value2"`
結構體 tag 由一個或多個鍵值對組成。鍵與值使用冒號 分隔,值用雙引號 括起來。同一個結構體字段可以設置多個鍵值對 tag,不同的鍵值對之間使用空格 分隔。
使用 json tag 指定字段名#
序列化與反序列化默認情況下使用結構體的字段名,我們可以通過給結構體字段添加 tag 來指定 json 序列化生成的字段名。
// 使用json tag指定序列化與反序列化時的行為
type Person struct {
Name string `json:"name"` // 指定json序列化/反序列化時使用小寫name
Age int64
Weight float64
}
忽略某個字段#
如果你想在 json 序列化 / 反序列化的時候忽略掉結構體中的某個字段,可以按如下方式在 tag 中添加-
。
// 使用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)。如果想要在序列序列化時忽略這些沒有值的字段時,可以在對應字段添加omitempty
tag。
舉個例子:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Hobby []string `json:"hobby"`
}
func omitemptyDemo() {
u1 := User{
Name: "七米",
}
// struct -> json string
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
輸出結果:
str:{"name":"七米","email":"","hobby":null}
如果想要在最終的序列化結果中去掉空值字段,可以像下面這樣定義結構體:
// 在tag中添加omitempty忽略空值
// 注意這裡 hobby,omitempty 合起來是json tag值,中間用英文逗號分隔
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
}
此時,再執行上述的omitemptyDemo
,輸出結果如下:
str:{"name":"七米"} // 序列化結果中沒有email和hobby字段
忽略嵌套結構體空值字段#
首先來看幾種結構體嵌套的示例:
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: "七米",
Hobby: []string{"足球", "双色球"},
}
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
匿名嵌套Profile
時序列化後的 json 串為單層的:
str:{"name":"七米","hobby":["足球","双色球"],"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":"七米","hobby":["足球","双色球"],"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":"七米","hobby":["足球","双色球"],"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":"七米","hobby":["足球","双色球"]}
不修改原結構體忽略空值字段#
我們需要 json 序列化User
,但是不想把密碼也序列化,又不想修改User
結構體,這個時候我們就可以使用創建另外一個結構體PublicUser
匿名嵌套原User
,同時指定Password
字段為匿名結構體指針類型,並添加omitempty
tag,示例代碼如下:
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: "七米",
Password: "123456",
}
b, err := json.Marshal(PublicUser{User: &u1})
if err != nil {
fmt.Printf("json.Marshal u1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b) // str:{"name":"七米"}
}
優雅處理字符串格式的數字#
有時候,前端在傳遞來的 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, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
fmt.Printf("str:%#v\n", string(b))
// json string -> map[string]interface{}
var m2 map[string]interface{}
err = json.Unmarshal(b, &m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // float64
}
這種場景下如果想更合理的處理數字就需要使用decoder
去反序列化,示例代碼如下:
func decoderDemo() {
// map[string]interface{} -> json string
var m = make(map[string]interface{}, 1)
m["count"] = 1 // int
b, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
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()
err = decoder.Decode(&m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // json.Number
// 將m2["count"]轉為json.Number之後調用Int64()方法獲得int64類型的值
count, err := m2["count"].(json.Number).Int64()
if err != nil {
fmt.Printf("parse to int64 failed, err:%v\n", err)
return
}
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, 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)
}
上面的代碼輸出結果如下:
str:{"create_time":"2020-04-05T12:28:06.799214+08:00"}
json.Unmarshal failed, err:parsing time ""2020-04-05 12:25:42"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"
也就是內置的 json 包不識別我們常用的字符串時間格式,如2020-04-05 12:25:42
。
不過我們通過實現 json.Marshaler
/json.Unmarshaler
接口實現自定義的事件格式解析。
type CustomTime struct {
time.Time
}
const ctLayout = "2006-01-02 15:04:05"
var nilTime = (time.Time{}).UnixNano()
func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), "\"")
if s == "null" {
ct.Time = time.Time{}
return
}
ct.Time, err = time.Parse(ctLayout, s)
return
}
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學習筆記》",
CreatedTime: time.Now(),
}
// 通過自定義的MarshalJSON方法實現struct -> json string
b, err := json.Marshal(&o1)
if err != nil {
fmt.Printf("json.Marshal o1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// 通過自定義的UnmarshalJSON方法實現json string -> struct
jsonStr := `{"created_time":"2020-04-05 10:18:20","id":123456,"title":"《七米的Go學習筆記》"}`
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學習筆記》"}
o2:main.Order{ID:123456, Title:"《七米的Go學習筆記》", 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: "七米",
}
// 使用匿名結構體內嵌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":"七米","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: "讚賞碼",
URL: "https://www.liwenzhou.com/images/zanshang_qr.jpg",
}
// struct -> json string
b, err := json.Marshal(struct {
*Comment
*Image
}{&c1, &i1})
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
jsonStr := `{"Content":"永遠不要高估自己","title":"讚賞碼","url":"https://www.liwenzhou.com/images/zanshang_qr.jpg"}`
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":"讚賞碼","url":"https://www.liwenzhou.com/images/zanshang_qr.jpg"}
c2:main.Comment{Content:"永遠不要高估自己"} i2:main.Image{Title:"讚賞碼", URL:"https://www.liwenzhou.com/images/zanshang_qr.jpg"}
處理不確定層級的 json#
如果 json 串沒有固定的格式導致不好定義與其相對應的結構體時,我們可以使用json.RawMessage
原始字節數據保存下來。
type sendMsg struct {
User string `json:"user"`
Msg string `json:"msg"`
}
func rawMessageDemo() {
jsonStr := `{"sendMsg":{"user":"q1mi","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:"q1mi", Msg:"永遠不要高估自己"}
}
參考鏈接:
https://stackoverflow.com/questions/25087960/json-unmarshal-time-that-isnt-in-rfc-3339-format
https://colobu.com/2017/06/21/json-tricks-in-Go/
https://stackoverflow.com/questions/11066946/partly-json-unmarshal-into-a-map-in-go