Golang?中的?json?編解碼深度解析
json 是我的老朋友,上份工作開(kāi)發(fā) web 應(yīng)用時(shí)就作為前后端數(shù)據(jù)交流的協(xié)議,現(xiàn)在也是用 json 數(shù)據(jù)持久化到數(shù)據(jù)庫(kù)。雖然面熟得很但還遠(yuǎn)遠(yuǎn)達(dá)不到知根知底,而且在邊界的探索上越發(fā)束手束腳。比如之前想寫(xiě)一個(gè)范型的結(jié)構(gòu)提高通用性,但是不清楚對(duì)范型的支持如何,思來(lái)想去還是用了普通類(lèi)型;還有項(xiàng)目中的規(guī)范不允許使用指針類(lèi)型的字段存儲(chǔ),我一直抱有疑問(wèn)。歸根結(jié)底還是不熟悉 json 編解碼的一些特性,導(dǎo)致我不敢嘗試也不敢使用,生怕出了問(wèn)題。所以近些日子也是狠狠研究了一把,補(bǔ)習(xí)了很多之前模棱兩可的概念。
有一句話(huà)說(shuō)的好:“多和舊人做新事”,我想我和 json 大概也屬于這種關(guān)系吧(?)
json 解析時(shí)字段名稱(chēng)保持一致
這個(gè)疑問(wèn)是,假如我們編碼不太規(guī)范,不給字段添加 Tag,序列化和反序列化后的字段字符串會(huì)是什么?
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}用代碼驗(yàn)證的結(jié)果是,json 編碼并不會(huì)將程序中定義的字段名稱(chēng)改成駝峰或者什么特殊大小寫(xiě)規(guī)則,而是完完全全使用原本的字符。如果是我目前的這個(gè)需求,即僅用來(lái)保存數(shù)據(jù),編碼和解碼都在后端進(jìn)行,那這樣完全可用不需要考慮更多,但如果是需要前后端數(shù)據(jù)對(duì)齊,而且有特殊的字段名稱(chēng)規(guī)范,那就要使用 tag 對(duì)編碼字段進(jìn)行規(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}但這只是編碼,對(duì)于解碼來(lái)說(shuō),是大小寫(xiě)不敏感的,就算傳過(guò)來(lái)的是某種形式的妖魔鬼怪也可以解析出來(lái),比如
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:
也因?yàn)槿绱?,最好不要在相關(guān)結(jié)構(gòu)體里定義名稱(chēng)相同的字段,即便有大小寫(xiě)的區(qū)別,也會(huì)導(dǎo)致不可預(yù)料的情況發(fā)生。而且嚴(yán)格按照駝峰格式命名的話(huà),不存在大小寫(xiě)區(qū)別,相同字母的字段就是唯一的。
而 Go 團(tuán)隊(duì)也將在 json/v2 中默認(rèn)大小寫(xiě)敏感,規(guī)范的行為肯定會(huì)帶來(lái)更少的 bug ~ 關(guān)于 json/v2 具體可以參考:A new experimental Go API for JSON。
哦哦還有一點(diǎn),如果不想某個(gè)字段參與解碼編碼可以使用特殊的 tag。
type Object struct {
Value string `json:"-"`
}可以編解碼接口和范型
我們知道 json 官方包底層是依靠反射實(shí)現(xiàn)的,所以獲取到傳入接口的結(jié)構(gòu)體類(lèi)型不是問(wèn)題,就可以使用原結(jié)構(gòu)體類(lèi)型去編解碼,所以只要是 Golang 支持的類(lèi)型都可以,甚至是范型。當(dāng)然也有一些反例需要注意,比如 func 這種類(lèi)型就不行。
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 和字段類(lèi)型
- 當(dāng)字段是結(jié)構(gòu)體類(lèi)型的,那么 omitempty 無(wú)效。
- 當(dāng)字段是指針類(lèi)型的,如果值是 nil,那么有 omitempty 就不進(jìn)行編碼,沒(méi)有 omitempty 會(huì)編碼成 null。
- 經(jīng)過(guò)測(cè)試不僅是指針類(lèi)型的結(jié)構(gòu)體,指針類(lèi)型的基礎(chǔ)類(lèi)型比如 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)體類(lèi)型和指針類(lèi)型性能比較
使用 Benchmark 測(cè)試結(jié)構(gòu)體類(lèi)型和指針類(lèi)型的性能。結(jié)論是在 CPU 性能上兩者差不多,但是一個(gè)指針類(lèi)型的字段會(huì)多進(jìn)行一次內(nèi)存分配,在一定程度上增加了 GC 的壓力,所以看起來(lái)小的結(jié)構(gòu)體還是結(jié)構(gòu)體值類(lèi)型更合適。
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 編解碼方式
可以實(shí)現(xiàn) json 規(guī)定的接口,使結(jié)構(gòu)體執(zhí)行特定的編解碼方式,假設(shè)下面一種情況,我希望業(yè)務(wù)代碼開(kāi)發(fā)中使用方便查詢(xún)和操作的map,然后存儲(chǔ)或者通訊使用占用空間更少的數(shù)組或者切片,但同時(shí)我又不想增加開(kāi)發(fā)人員的心智負(fù)擔(dān),想要之前怎么使用現(xiàn)在就如何使用,或者無(wú)法更改一些庫(kù)的執(zhí)行方式只能繞路。也就是說(shuō)平時(shí)開(kāi)發(fā)時(shí)需要直接調(diào)用 json.Marshal 或 json.UnMarshal,而不需要額外操作,這時(shí)就可以通過(guò)實(shí)現(xiàn)接口的方式達(dá)成目的,見(jià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":{}}}此處還有一個(gè)小 Tips,UnmarshalJSON 用指針接收器沒(méi)問(wèn)題,因?yàn)樾枰薷恼{(diào)用這個(gè)方法的結(jié)構(gòu)體的字段值,但是 MarshalJSON 盡量用值接收器,因?yàn)檫@樣在調(diào)用 json.Marshal 時(shí)無(wú)論傳入的是值還是指針都能正常編碼,同時(shí)也避免了傳入的是 nil 導(dǎo)致 panic。
被遺忘在角落的 gob
在 golang 源碼的 encoding 包下有很多編解碼方式,比如 json、xml、base64 等等,但其中也有一個(gè) gob,假如你之前沒(méi)有接觸過(guò) golang 這門(mén)編程語(yǔ)言那你大概率沒(méi)有聽(tīng)說(shuō)過(guò)這種編碼解碼方式,因?yàn)樗酮?dú)屬于 golang,其他語(yǔ)言基本上可以說(shuō)無(wú)法解析。
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 的行為相比需要依賴(lài) bytes.Buffer,也正因如此可以連續(xù)向 Buffer 編碼多個(gè)結(jié)構(gòu)體,然后連續(xù)解碼多個(gè)結(jié)構(gòu)體。此外和 json 一樣也可以實(shí)現(xiàn)特定的接口來(lái)自定義編解碼行為,具體可以參考https://pkg.go.dev/encoding/gob。
向 json 和 xml 這種編碼方式方便讓我們?nèi)庋塾^(guān)察,但因此也犧牲了性能和空間,而 gob 類(lèi)似 protobuf 都是生成二進(jìn)制,但是 gob 僅存在于 golang 生態(tài)中,普及度遠(yuǎn)遠(yuǎn)不及可以生成多種語(yǔ)言代碼的 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è)計(jì)了相同的結(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ǎn)單結(jié)構(gòu)體,gob 和 json 在 CPU 性能上并沒(méi)有看到什么差距,但是內(nèi)存分配差了蠻多,如果不考慮通用性和擴(kuò)展性的話(huà),gob 也是個(gè)不錯(cuò)的選擇,雖然事實(shí)是這兩方面不可能不考慮。而且在性能方面也遠(yuǎn)遠(yuǎn)不及代碼生成派,生產(chǎn)實(shí)踐中多多用 protobuf 才是正道。
RawMessage 的應(yīng)用場(chǎng)景
試想這樣一種情況,某個(gè)推薦業(yè)務(wù)有兩層分別是 A 和 B ,通常是是 A 調(diào)用 B 的接口(RPC),然后 A 再組織數(shù)據(jù)發(fā)給前端,QA和運(yùn)營(yíng)需求要獲取到 B 持有的信息用來(lái) debug 和測(cè)試,這個(gè)時(shí)候因?yàn)槭遣魂P(guān)鍵的 debug 信息所以也就懶得定義消息結(jié)構(gòu)體,而是直接在B中用 json 將數(shù)據(jù)序列化成字符串傳給 A,然后 A 在外面封裝一層錯(cuò)誤碼和數(shù)據(jù)傳給前端,如果直接這么操作會(huì)有一個(gè)問(wèn)題:
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\"}"}字符串類(lèi)型的字段在 json.Marshal 時(shí),其中的雙引號(hào)會(huì)被轉(zhuǎn)義,甚至于三層四層來(lái)回傳遞后轉(zhuǎn)移符號(hào)會(huì)越來(lái)越多。所以這個(gè)時(shí)候就可以使用 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"}}除了編碼之外,解碼時(shí)的 RawMessage 也有大用處,尤其是需要二次解碼的情況。比如有一個(gè)接口是聊天室發(fā)送消息,然后消息有不同的類(lèi)型,每個(gè)類(lèi)型的內(nèi)容的結(jié)構(gòu)都不一樣,這時(shí)需要先解碼通用結(jié)構(gòu),然后拿到消息類(lèi)型,再根據(jù)消息類(lèi)型解碼具體消息內(nèi)容。比如下面這個(gè)例子,如果不使用 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}新時(shí)代的明星 json v2
從 https://pkg.go.dev/encoding/json?tab=versions 中可以看到,json 包在 go1 也就是最初的版本就已經(jīng)存在了,只是當(dāng)時(shí)有一些設(shè)計(jì)和特性放到當(dāng)下來(lái)看是有些老舊的,由于 Go 的兼容性承諾也不便對(duì)其進(jìn)行大刀闊斧的改動(dòng),正是因?yàn)槿绱耍谧罱陌姹局?go 團(tuán)隊(duì)推出了新的 json 包也就是 json/v2 來(lái)解決 json 編解碼的一些痛點(diǎn)問(wèn)題。如果對(duì)具體內(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)。
會(huì)用 v2 實(shí)現(xiàn) v1,只是 v1 中原本的一些特性在 v2 中會(huì)變成可選擇的 Option 提供出來(lái)以保證兼容性,這些選項(xiàng)不乏上文提到的一些特殊性質(zhì),譬如:
- 編解碼結(jié)構(gòu)體時(shí)字段大小寫(xiě)敏感 (case-sensitive)
- omitempty 起作用的對(duì)象會(huì)發(fā)生變化
- nil 的 slice 和 map 會(huì)編碼成空數(shù)組和空結(jié)構(gòu)體而不是 null
- 以及其他的一些性質(zhì)
當(dāng)然不只是一些編解碼行為發(fā)生了變化,性能方面也有了很大提高,甚至還能看到專(zhuān)門(mén)的文章介紹和分析當(dāng)前社區(qū)流行的諸多 json 庫(kù)和 json/v2 的對(duì)比,老熟人 sonic 也在其中,具體內(nèi)容詳見(jiàn) [https://github.com/go-json-experiment/jsonbench](JSON Benchmarks)。
到此這篇關(guān)于重新認(rèn)識(shí) Golang 中的 json 編解碼的文章就介紹到這了,更多相關(guān)Golang 中的 json 編解碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Golang中自定義json序列化時(shí)間格式的示例代碼
- Golang HTTP請(qǐng)求Json響應(yīng)解析方法以及解讀失敗的原因
- golang(gin)的全局統(tǒng)一異常處理方式,并統(tǒng)一返回json
- golang使用mapstructure解析json
- Golang中g(shù)in框架綁定解析json數(shù)據(jù)的兩種方法
- 使用golang進(jìn)行http,get或postJson請(qǐng)求
- Golang中json和jsoniter的區(qū)別使用示例
- Golang中空的切片轉(zhuǎn)化成 JSON 后變?yōu)?nbsp;null 問(wèn)題的解決方案
- 詳解golang中的結(jié)構(gòu)體編解碼神器Mapstructure庫(kù)
相關(guān)文章
golang實(shí)現(xiàn)通過(guò)smtp發(fā)送電子郵件的方法
這篇文章主要介紹了golang實(shí)現(xiàn)通過(guò)smtp發(fā)送電子郵件的方法,實(shí)例分析了Go語(yǔ)言基于SMTP協(xié)議發(fā)送郵件的相關(guān)技巧,需要的朋友可以參考下2016-07-07
Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過(guò)程詳解
這篇文章主要介紹了Go結(jié)合反射將結(jié)構(gòu)體轉(zhuǎn)換成Excel的過(guò)程詳解,大概思路是在Go的結(jié)構(gòu)體中每個(gè)屬性打上一個(gè)excel標(biāo)簽,利用反射獲取標(biāo)簽中的內(nèi)容,作為表格的Header,需要的朋友可以參考下2022-06-06
golang標(biāo)準(zhǔn)庫(kù)SSH操作示例詳解
文章介紹了如何使用Golang的crypto/ssh庫(kù)實(shí)現(xiàn)SSH客戶(hù)端功能,包括連接遠(yuǎn)程服務(wù)器、執(zhí)行命令、捕獲輸出以及與os/exec標(biāo)準(zhǔn)庫(kù)的對(duì)比,本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-12-12
Golang實(shí)現(xiàn)讀取ZIP壓縮包并顯示Gin靜態(tài)html網(wǎng)站
這篇文章主要為大家詳細(xì)介紹了如何通過(guò)Golang實(shí)現(xiàn)從ZIP壓縮包讀取內(nèi)容并作為Gin靜態(tài)網(wǎng)站顯示,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-07-07
Go實(shí)現(xiàn)JWT認(rèn)證中間件的項(xiàng)目實(shí)戰(zhàn)
本文將介紹在Gin框架中實(shí)現(xiàn)完整的JWT認(rèn)證方案,同時(shí)包含靈活的?Redis?集成選項(xiàng),文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-08-08
15個(gè)Golang中時(shí)間處理的實(shí)用函數(shù)
在Go編程中,處理日期和時(shí)間是一項(xiàng)常見(jiàn)任務(wù),涉及到精確性和靈活性,本文將介紹一系列實(shí)用函數(shù),它們充當(dāng)time包的包裝器,需要的可以參考下2024-01-01

