Golang?中的?json?編解碼深度解析
json 是我的老朋友,上份工作開發(fā) web 應(yīng)用時就作為前后端數(shù)據(jù)交流的協(xié)議,現(xiàn)在也是用 json 數(shù)據(jù)持久化到數(shù)據(jù)庫。雖然面熟得很但還遠遠達不到知根知底,而且在邊界的探索上越發(fā)束手束腳。比如之前想寫一個范型的結(jié)構(gòu)提高通用性,但是不清楚對范型的支持如何,思來想去還是用了普通類型;還有項目中的規(guī)范不允許使用指針類型的字段存儲,我一直抱有疑問。歸根結(jié)底還是不熟悉 json 編解碼的一些特性,導(dǎo)致我不敢嘗試也不敢使用,生怕出了問題。所以近些日子也是狠狠研究了一把,補習(xí)了很多之前模棱兩可的概念。
有一句話說的好:“多和舊人做新事”,我想我和 json 大概也屬于這種關(guān)系吧(?)
json 解析時字段名稱保持一致
這個疑問是,假如我們編碼不太規(guī)范,不給字段添加 Tag,序列化和反序列化后的字段字符串會是什么?
type Object struct {
ID string
VaLuE2T int64
}
func TestFunc(t *testing.T) {
obj := Object{
ID: "the-id",
VaLuE2T: 7239,
}
marshal, err := json.Marshal(obj)
assert.Nil(t, err)
fmt.Println(string(marshal))
}{"ID":"the-id","VaLuE2T":7239}用代碼驗證的結(jié)果是,json 編碼并不會將程序中定義的字段名稱改成駝峰或者什么特殊大小寫規(guī)則,而是完完全全使用原本的字符。如果是我目前的這個需求,即僅用來保存數(shù)據(jù),編碼和解碼都在后端進行,那這樣完全可用不需要考慮更多,但如果是需要前后端數(shù)據(jù)對齊,而且有特殊的字段名稱規(guī)范,那就要使用 tag 對編碼字段進行規(guī)定,比如下方的代碼。
type Object struct {
ID string `json:"id"`
VaLuE2T int64 `json:"value2t"`
}
func TestFunc(t *testing.T) {
obj := Object{
ID: "the-id",
VaLuE2T: 7239,
}
marshal, err := json.Marshal(obj)
assert.Nil(t, err)
fmt.Println(string(marshal))
}{"id":"the-id","value2t":7239}但這只是編碼,對于解碼來說,是大小寫不敏感的,就算傳過來的是某種形式的妖魔鬼怪也可以解析出來,比如
type Object struct {
CaSeTesT string
CAsEteSt string
}
func TestFunc(t *testing.T) {
newObj := Object{}
testString := `{"cAsEteSt":"test"}`
err := json.Unmarshal([]byte(testString), &newObj)
assert.Nil(t, err)
fmt.Println("CaSeTesT:", newObj.CaSeTesT, " CAsEteSt:", newObj.CAsEteSt)
}CaSeTesT: test CAsEteSt:
也因為如此,最好不要在相關(guān)結(jié)構(gòu)體里定義名稱相同的字段,即便有大小寫的區(qū)別,也會導(dǎo)致不可預(yù)料的情況發(fā)生。而且嚴格按照駝峰格式命名的話,不存在大小寫區(qū)別,相同字母的字段就是唯一的。
而 Go 團隊也將在 json/v2 中默認大小寫敏感,規(guī)范的行為肯定會帶來更少的 bug ~ 關(guān)于 json/v2 具體可以參考:A new experimental Go API for JSON。
哦哦還有一點,如果不想某個字段參與解碼編碼可以使用特殊的 tag。
type Object struct {
Value string `json:"-"`
}可以編解碼接口和范型
我們知道 json 官方包底層是依靠反射實現(xiàn)的,所以獲取到傳入接口的結(jié)構(gòu)體類型不是問題,就可以使用原結(jié)構(gòu)體類型去編解碼,所以只要是 Golang 支持的類型都可以,甚至是范型。當(dāng)然也有一些反例需要注意,比如 func 這種類型就不行。
type Object struct {
Func func()
}
func TestFunc(t *testing.T) {
obj := Object{
Func: func() {},
}
marshal, err := json.Marshal(obj)
fmt.Println(err)
}json: unsupported type: func()
omitempty 和字段類型
- 當(dāng)字段是結(jié)構(gòu)體類型的,那么 omitempty 無效。
- 當(dāng)字段是指針類型的,如果值是 nil,那么有 omitempty 就不進行編碼,沒有 omitempty 會編碼成 null。
- 經(jīng)過測試不僅是指針類型的結(jié)構(gòu)體,指針類型的基礎(chǔ)類型比如 string 或者 int64 也是如此。
type Object struct {
TheStructO AObject `json:"theStructO,omitempty"`
TheStruct AObject `json:"theStruct"`
ThePointO *AObject `json:"thePointO,omitempty"`
ThePoint *AObject `json:"thePoint"`
}
type AObject struct {
Values interface{}
}
func TestFunc(t *testing.T) {
obj := Object{}
marshal, err := json.Marshal(obj)
assert.Nil(t, err)
fmt.Println(string(marshal))
}{"theStructO":{"Values":null},"theStruct":{"Values":null},"thePoint":null}結(jié)構(gòu)體類型和指針類型性能比較
使用 Benchmark 測試結(jié)構(gòu)體類型和指針類型的性能。結(jié)論是在 CPU 性能上兩者差不多,但是一個指針類型的字段會多進行一次內(nèi)存分配,在一定程度上增加了 GC 的壓力,所以看起來小的結(jié)構(gòu)體還是結(jié)構(gòu)體值類型更合適。
type ObjectStruct struct {
TheStruct AObject `json:"theStruct"`
}
type ObjectPoint struct {
TheStruct *AObject `json:"theStruct"`
}
func BenchmarkFunc(b *testing.B) {
data := []byte(`{"theStruct":{"valueString":"text","valueInt":123,"valueFloat":3.14}}`)
b.Run("unmarshal-struct", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(data, &ObjectStruct{})
}
})
b.Run("unmarshal-point", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(data, &ObjectPoint{})
}
})
}BenchmarkFunc BenchmarkFunc/unmarshal-struct BenchmarkFunc/unmarshal-struct-8 457996 2518 ns/op 304 B/op 8 allocs/op BenchmarkFunc/unmarshal-point BenchmarkFunc/unmarshal-point-8 471489 2517 ns/op 312 B/op 9 allocs/op PASS
自定義 json 編解碼方式
可以實現(xiàn) json 規(guī)定的接口,使結(jié)構(gòu)體執(zhí)行特定的編解碼方式,假設(shè)下面一種情況,我希望業(yè)務(wù)代碼開發(fā)中使用方便查詢和操作的map,然后存儲或者通訊使用占用空間更少的數(shù)組或者切片,但同時我又不想增加開發(fā)人員的心智負擔(dān),想要之前怎么使用現(xiàn)在就如何使用,或者無法更改一些庫的執(zhí)行方式只能繞路。也就是說平時開發(fā)時需要直接調(diào)用 json.Marshal 或 json.UnMarshal,而不需要額外操作,這時就可以通過實現(xiàn)接口的方式達成目的,見如下代碼。
type Object struct {
UserMap map[string]struct{}
}
func (o Object) MarshalJSON() ([]byte, error) {
list := make([]string, 0, len(o.UserMap))
for key := range o.UserMap {
list = append(list, key)
}
return json.Marshal(list)
}
func (o *Object) UnmarshalJSON(b []byte) error {
var list []string
err := json.Unmarshal(b, &list)
if err != nil {
return err
}
o.UserMap = make(map[string]struct{}, len(list))
for i := range list {
o.UserMap[list[i]] = struct{}{}
}
return nil
}
type ObjectNormal struct {
UserMap map[string]struct{}
}
func TestFunc(t *testing.T) {
userMap := map[string]struct{}{
"user1": {},
"user2": {},
"user3": {},
}
obj1 := &Object{
UserMap: userMap,
}
obj2 := &ObjectNormal{
UserMap: userMap,
}
marshal1, err := json.Marshal(obj1)
assert.Nil(t, err)
fmt.Println("len:", len(marshal1), string(marshal1))
marshal2, err := json.Marshal(obj2)
assert.Nil(t, err)
fmt.Println("len:", len(marshal2), string(marshal2))
}len: 25 ["user1","user2","user3"]
len: 46 {"UserMap":{"user1":{},"user2":{},"user3":{}}}此處還有一個小 Tips,UnmarshalJSON 用指針接收器沒問題,因為需要修改調(diào)用這個方法的結(jié)構(gòu)體的字段值,但是 MarshalJSON 盡量用值接收器,因為這樣在調(diào)用 json.Marshal 時無論傳入的是值還是指針都能正常編碼,同時也避免了傳入的是 nil 導(dǎo)致 panic。
被遺忘在角落的 gob
在 golang 源碼的 encoding 包下有很多編解碼方式,比如 json、xml、base64 等等,但其中也有一個 gob,假如你之前沒有接觸過 golang 這門編程語言那你大概率沒有聽說過這種編碼解碼方式,因為它就獨屬于 golang,其他語言基本上可以說無法解析。
type G struct {
Value string
}
func TestGOB(t *testing.T) {
g := &G{Value: "hello"}
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(g); err != nil {
panic(err)
}
fmt.Println("Gob encoded bytes:", buf.Bytes())
var decoded G
dec := gob.NewDecoder(&buf)
if err := dec.Decode(&decoded); err != nil {
panic(err)
}
fmt.Println("Decoded struct:", decoded)
}使用方式大差不差,但與 json 的行為相比需要依賴 bytes.Buffer,也正因如此可以連續(xù)向 Buffer 編碼多個結(jié)構(gòu)體,然后連續(xù)解碼多個結(jié)構(gòu)體。此外和 json 一樣也可以實現(xiàn)特定的接口來自定義編解碼行為,具體可以參考https://pkg.go.dev/encoding/gob。
向 json 和 xml 這種編碼方式方便讓我們?nèi)庋塾^察,但因此也犧牲了性能和空間,而 gob 類似 protobuf 都是生成二進制,但是 gob 僅存在于 golang 生態(tài)中,普及度遠遠不及可以生成多種語言代碼的 protobuf。
type User struct {
Name string
}
func Benchmark(b *testing.B) {
b.Run("gob", func(b *testing.B) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
user := User{Name: "hello"}
for i := 0; i < b.N; i++ {
_ = enc.Encode(user)
_ = dec.Decode(&user)
}
})
b.Run("json", func(b *testing.B) {
user := User{Name: "hello"}
for i := 0; i < b.N; i++ {
marshal, _ := json.Marshal(user)
_ = json.Unmarshal(marshal, &user)
}
})
b.Run("protobuf", func(b *testing.B) {
user := ttt.User{Name: "hello"}
for i := 0; i < b.N; i++ {
data, _ := proto.Marshal(&user)
_ = proto.Unmarshal(data, &user)
}
})
}控制變量法,我設(shè)計了相同的結(jié)構(gòu)體 proto。
message User {
string Name = 1;
}Benchmark Benchmark/gob Benchmark/gob-8 1230975 954.7 ns/op 32 B/op 3 allocs/op Benchmark/json Benchmark/json-8 1000000 1130 ns/op 256 B/op 7 allocs/op Benchmark/protobuf Benchmark/protobuf-8 2500924 483.2 ns/op 16 B/op 2 allocs/op PASS
可能是由于我用的是簡單結(jié)構(gòu)體,gob 和 json 在 CPU 性能上并沒有看到什么差距,但是內(nèi)存分配差了蠻多,如果不考慮通用性和擴展性的話,gob 也是個不錯的選擇,雖然事實是這兩方面不可能不考慮。而且在性能方面也遠遠不及代碼生成派,生產(chǎn)實踐中多多用 protobuf 才是正道。
RawMessage 的應(yīng)用場景
試想這樣一種情況,某個推薦業(yè)務(wù)有兩層分別是 A 和 B ,通常是是 A 調(diào)用 B 的接口(RPC),然后 A 再組織數(shù)據(jù)發(fā)給前端,QA和運營需求要獲取到 B 持有的信息用來 debug 和測試,這個時候因為是不關(guān)鍵的 debug 信息所以也就懶得定義消息結(jié)構(gòu)體,而是直接在B中用 json 將數(shù)據(jù)序列化成字符串傳給 A,然后 A 在外面封裝一層錯誤碼和數(shù)據(jù)傳給前端,如果直接這么操作會有一個問題:
type ResponseB struct {
Name string
}
type ResponseA struct {
Data string
}
func TestRaw(t *testing.T) {
r := ResponseB{
Name: "hello-world",
}
marshal, err := json.Marshal(r)
assert.Nil(t, err)
ra := &ResponseA{
Data: string(marshal),
}
marshal2, err := json.Marshal(ra)
assert.Nil(t, err)
fmt.Println(string(marshal), string(marshal2))
}{"Name":"hello-world"} {"Data":"{\"Name\":\"hello-world\"}"}字符串類型的字段在 json.Marshal 時,其中的雙引號會被轉(zhuǎn)義,甚至于三層四層來回傳遞后轉(zhuǎn)移符號會越來越多。所以這個時候就可以使用 json.RawMessage。
type ResponseB struct {
Name string
}
type ResponseA struct {
Data json.RawMessage
}
func TestRaw(t *testing.T) {
r := RawStruct{
Name: "hello-world",
}
marshal, err := json.Marshal(r)
assert.Nil(t, err)
rj := &RawJson{
Data: json.RawMessage(marshal),
}
marshal3, err := json.Marshal(rj)
assert.Nil(t, err)
fmt.Println(string(marshal), string(marshal3))
}{"Name":"hello-world"} {"Data":{"Name":"hello-world"}}除了編碼之外,解碼時的 RawMessage 也有大用處,尤其是需要二次解碼的情況。比如有一個接口是聊天室發(fā)送消息,然后消息有不同的類型,每個類型的內(nèi)容的結(jié)構(gòu)都不一樣,這時需要先解碼通用結(jié)構(gòu),然后拿到消息類型,再根據(jù)消息類型解碼具體消息內(nèi)容。比如下面這個例子,如果不使用 RawMessage,就一定要在字符串內(nèi)增加轉(zhuǎn)義。
type Inside struct {
Name string
}
type Outside struct {
Data interface{}
DataString string
DataRaw json.RawMessage
}
func TestRaw(t *testing.T) {
data := `{"Data":"{"Name":"hello-world"}","DataString":"{"Name":"hello-world"}","DataRaw":{"Name":"hello-world"}}`
rj := Outside{}
err := json.Unmarshal([]byte(data), &rj)
assert.Nil(t, err)
fmt.Println(rj)
}Expected nil, but got: &json.SyntaxError{msg:"invalid character 'N' after object key:value pair", Offset:12}新時代的明星 json v2
從 https://pkg.go.dev/encoding/json?tab=versions 中可以看到,json 包在 go1 也就是最初的版本就已經(jīng)存在了,只是當(dāng)時有一些設(shè)計和特性放到當(dāng)下來看是有些老舊的,由于 Go 的兼容性承諾也不便對其進行大刀闊斧的改動,正是因為如此,在最近的版本中 go 團隊推出了新的 json 包也就是 json/v2 來解決 json 編解碼的一些痛點問題。如果對具體內(nèi)容感興趣可以去閱讀官方的文檔 https://pkg.go.dev/encoding/json/v2,包括 v1 版本和 v2 版本的一些區(qū)別 https://pkg.go.dev/encoding/json#hdr-Migrating_to_v2,以及介紹新版本 json 的博客 [https://go.dev/blog/jsonv2-exp](A new experimental Go API for JSON)。
會用 v2 實現(xiàn) v1,只是 v1 中原本的一些特性在 v2 中會變成可選擇的 Option 提供出來以保證兼容性,這些選項不乏上文提到的一些特殊性質(zhì),譬如:
- 編解碼結(jié)構(gòu)體時字段大小寫敏感 (case-sensitive)
- omitempty 起作用的對象會發(fā)生變化
- nil 的 slice 和 map 會編碼成空數(shù)組和空結(jié)構(gòu)體而不是 null
- 以及其他的一些性質(zhì)
當(dāng)然不只是一些編解碼行為發(fā)生了變化,性能方面也有了很大提高,甚至還能看到專門的文章介紹和分析當(dāng)前社區(qū)流行的諸多 json 庫和 json/v2 的對比,老熟人 sonic 也在其中,具體內(nèi)容詳見 [https://github.com/go-json-experiment/jsonbench](JSON Benchmarks)。
到此這篇關(guān)于重新認識 Golang 中的 json 編解碼的文章就介紹到這了,更多相關(guān)Golang 中的 json 編解碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Golang中自定義json序列化時間格式的示例代碼
- Golang HTTP請求Json響應(yīng)解析方法以及解讀失敗的原因
- golang(gin)的全局統(tǒng)一異常處理方式,并統(tǒng)一返回json
- golang使用mapstructure解析json
- Golang中g(shù)in框架綁定解析json數(shù)據(jù)的兩種方法
- 使用golang進行http,get或postJson請求
- Golang中json和jsoniter的區(qū)別使用示例
- Golang中空的切片轉(zhuǎn)化成 JSON 后變?yōu)?nbsp;null 問題的解決方案
- 詳解golang中的結(jié)構(gòu)體編解碼神器Mapstructure庫
相關(guān)文章
golang實現(xiàn)通過smtp發(fā)送電子郵件的方法
這篇文章主要介紹了golang實現(xiàn)通過smtp發(fā)送電子郵件的方法,實例分析了Go語言基于SMTP協(xié)議發(fā)送郵件的相關(guān)技巧,需要的朋友可以參考下2016-07-07
Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過程詳解
這篇文章主要介紹了Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過程詳解,大概思路是在Go的結(jié)構(gòu)體中每個屬性打上一個excel標(biāo)簽,利用反射獲取標(biāo)簽中的內(nèi)容,作為表格的Header,需要的朋友可以參考下2022-06-06
Golang實現(xiàn)讀取ZIP壓縮包并顯示Gin靜態(tài)html網(wǎng)站
這篇文章主要為大家詳細介紹了如何通過Golang實現(xiàn)從ZIP壓縮包讀取內(nèi)容并作為Gin靜態(tài)網(wǎng)站顯示,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-07-07

