golang JSON技巧

技术颜良發表於2024-11-26

目錄

  1. 臨時忽略struct空欄位
  2. 臨時新增額外的欄位
  3. 臨時粘合兩個struct
  4. 一個json切分成兩個struct
  5. 臨時改名struct的欄位
  6. 用字串傳遞數字
  7. 容忍字串和數字互轉
  8. 容忍空陣列作為物件
  9. 使用 MarshalJSON支援time.Time
  10. 使用 RegisterTypeEncoder支援time.Time
  11. 使用 MarshalText支援非字串作為key的map
  12. 使用 json.RawMessage
  13. 使用 json.Number
  14. 統一更改欄位的命名風格
  15. 使用私有的欄位
  16. 忽略掉一些欄位
  17. 忽略掉一些欄位2

有的時候上游傳過來的欄位是string型別的,但是我們卻想用變成數字來使用. 本來用一個json:”,string” 就可以支援了,如果不知道golang的這些小技巧,就要大費周章了.

臨時忽略struct空欄位

type User struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    // many more fields…
}

如果想臨時忽略掉空Password欄位,可以用omitempty:

json.Marshal(struct {
    *User
    Password bool `json:"password,omitempty"`
}{
    User: user,
})

臨時新增額外的欄位

type User struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    // many more fields…
}

臨時忽略掉空Password欄位,並且新增token欄位

json.Marshal(struct {
    *User
    Token    string `json:"token"`
    Password bool `json:"password,omitempty"`
}{
    User: user,
    Token: token,
})

臨時粘合兩個struct

透過嵌入struct的方式:

type BlogPost struct {
    URL   string `json:"url"`
    Title string `json:"title"`
}
type Analytics struct {
    Visitors  int `json:"visitors"`
    PageViews int `json:"page_views"`
}
json.Marshal(struct{
    *BlogPost
    *Analytics
}{post, analytics})

一個json切分成兩個struct

json.Unmarshal([]byte(`{
  "url": "attila@attilaolah.eu",
  "title": "Attila's Blog",
  "visitors": 6,
  "page_views": 14
}`), &struct {
  *BlogPost
  *Analytics
}{&post, &analytics})

臨時改名struct的欄位

type CacheItem struct {
    Key    string `json:"key"`
    MaxAge int    `json:"cacheAge"`
    Value  Value  `json:"cacheValue"`
}
json.Marshal(struct{
    *CacheItem
    // Omit bad keys
    OmitMaxAge omit `json:"cacheAge,omitempty"`
    OmitValue  omit `json:"cacheValue,omitempty"`
    // Add nice keys
    MaxAge int    `json:"max_age"`
    Value  *Value `json:"value"`
}{
    CacheItem: item,
    // Set the int by value:
    MaxAge: item.MaxAge,
    // Set the nested struct by reference, avoid making a copy:
    Value: &item.Value,
})

用字串傳遞數字

type TestObject struct {
    Field1 int    `json:",string"`
}

這個對應的json是 {"Field1": "100"}

如果json是 {"Field1": 100} 則會報錯

容忍字串和數字互轉

如果您使用的是jsoniter,可以啟動模糊模式來支援 PHP 傳遞過來的 JSON.

import "github.com/json-iterator/go/extra"
extra.RegisterFuzzyDecoders()

這樣就可以處理字串和數字型別不對的問題了.比如

var val string
jsoniter.UnmarshalFromString(`100`, &val)

又比如

var val float32
jsoniter.UnmarshalFromString(`"1.23"`, &val)

容忍空陣列作為物件

PHP另外一個令人崩潰的地方是,如果 PHP array是空的時候,序列化出來是[].但是不為空的時候,序列化出來的是{"key":"value"}. 我們需要把 [] 當成 {} 處理.

如果您使用的是jsoniter,可以啟動模糊模式來支援 PHP 傳遞過來的 JSON.

import "github.com/json-iterator/go/extra"
extra.RegisterFuzzyDecoders()

這樣就可以支援了

var val map[string]interface{}
jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支援time.Time

golang 預設會把 time.Time 用字串方式序列化.如果我們想用其他方式表示 time.Time,需要自定義型別並定義 MarshalJSON.

type timeImplementedMarshaler time.Time
func (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {
    seconds := time.Time(obj).Unix()
    return []byte(strconv.FormatInt(seconds, 10)), nil
}

序列化的時候會呼叫 MarshalJSON

type TestObject struct {
    Field timeImplementedMarshaler
}
should := require.New(t)
val := timeImplementedMarshaler(time.Unix(123, 0))
obj := TestObject{val}
bytes, err := jsoniter.Marshal(obj)
should.Nil(err)
should.Equal(`{"Field":123}`, string(bytes))

使用 RegisterTypeEncoder支援time.Time

jsoniter 能夠對不是您定義的type自定義JSON編解碼方式.比如對於 time.Time 可以用 epoch int64 來序列化

import "github.com/json-iterator/go/extra"
extra.RegisterTimeAsInt64Codec(time.Microsecond)
output, err := jsoniter.Marshal(time.Unix(1, 1002))
should.Equal("1000001", string(output))

如果要自定義的話,參見 RegisterTimeAsInt64Codec 的實現程式碼

使用 MarshalText支援非字串作為key的map

雖然 JSON 標準裡只支援 string 作為 keymap.但是 golang 透過 MarshalText() 介面,使得其他型別也可以作為 mapkey.例如

f, _, _ := big.ParseFloat("1", 10, 64, big.ToZero)
val := map[*big.Float]string{f: "2"}
str, err := MarshalToString(val)
should.Equal(`{"1":"2"}`, str)

其中 big.Float 就實現了 MarshalText()

使用 json.RawMessage

如果部分json文件沒有標準格式,我們可以把原始的資訊用[]byte儲存下來.

type TestObject struct {
    Field1 string
    Field2 json.RawMessage
}
var data TestObject
json.Unmarshal([]byte(`{"field1": "hello", "field2": [1,2,3]}`), &data)
should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

預設情況下,如果是 interface{} 對應數字的情況會是 float64 型別的.如果輸入的數字比較大,這個表示會有損精度.所以可以 UseNumber() 啟用 json.Number 來用字串表示數字.

decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))
decoder1.UseNumber()
var obj1 interface{}
decoder1.Decode(&obj1)
should.Equal(json.Number("123"), obj1)

jsoniter 支援標準庫的這個用法.同時,擴充套件了行為使得 Unmarshal 也可以支援 UseNumber 了.

json := Config{UseNumber:true}.Froze()
var obj interface{}
json.UnmarshalFromString("123", &obj)
should.Equal(json.Number("123"), obj)

統一更改欄位的命名風格

經常 JSON 裡的欄位名 Go 裡的欄位名是不一樣的.我們可以用 field tag 來修改.

output, err := jsoniter.Marshal(struct {
    UserName      string `json:"user_name"`
    FirstLanguage string `json:"first_language"`
}{
    UserName:      "taowen",
    FirstLanguage: "Chinese",
})
should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

但是一個個欄位來設定,太麻煩了.如果使用 jsoniter,我們可以統一設定命名風格.

import "github.com/json-iterator/go/extra"
extra.SetNamingStrategy(LowerCaseWithUnderscores)
output, err := jsoniter.Marshal(struct {
    UserName      string
    FirstLanguage string
}{
    UserName:      "taowen",
    FirstLanguage: "Chinese",
})
should.Nil(err)
should.Equal(`{"user_name":"taowen","first_language":"Chinese"}`, string(output))

使用私有的欄位

Go 的標準庫只支援 public 的 field.jsoniter 額外支援了 private 的 field.需要使用 SupportPrivateFields() 來開啟開關.

import "github.com/json-iterator/go/extra"
extra.SupportPrivateFields()
type TestObject struct {
    field1 string
}
obj := TestObject{}
jsoniter.UnmarshalFromString(`{"field1":"Hello"}`, &obj)
should.Equal("Hello", obj.field1)

下面是我補充的內容

忽略掉一些欄位

原文中第一節有個錯誤,我更正過來了.omitempty不會忽略某個欄位,而是忽略空的欄位,當欄位的值為空值的時候,它不會出現在JSON資料中.

如果想忽略某個欄位,需要使用 json:"-"格式.

type User struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    // many more fields…
}

如果想臨時忽略掉空Password欄位,可以用-:

json.Marshal(struct {
    *User
    Password bool `json:"-"`
}{
    User: user,
})

忽略掉一些欄位2

如果不想更改原struct,還可以使用下面的方法:

type User struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    // many more fields…
}
type omit *struct{}
type PublicUser struct {
    *User
    Password omit `json:"-"`
}
json.Marshal(PublicUser{
    User: user,
})

相關文章