最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Golang?中的?json?編解碼深度解析

 更新時(shí)間:2025年10月19日 08:59:39   作者:Innsane  
本文帶領(lǐng)大家重新認(rèn)識(shí)Golang中的json編解碼,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧

json 是我的老朋友,上份工作開發(fā) web 應(yīng)用時(shí)就作為前后端數(shù)據(jù)交流的協(xié)議,現(xiàn)在也是用 json 數(shù)據(jù)持久化到數(shù)據(jù)庫(kù)。雖然面熟得很但還遠(yuǎn)遠(yuǎn)達(dá)不到知根知底,而且在邊界的探索上越發(fā)束手束腳。比如之前想寫一個(gè)范型的結(jié)構(gòu)提高通用性,但是不清楚對(duì)范型的支持如何,思來想去還是用了普通類型;還有項(xiàng)目中的規(guī)范不允許使用指針類型的字段存儲(chǔ),我一直抱有疑問。歸根結(jié)底還是不熟悉 json 編解碼的一些特性,導(dǎo)致我不敢嘗試也不敢使用,生怕出了問題。所以近些日子也是狠狠研究了一把,補(bǔ)習(xí)了很多之前模棱兩可的概念。

有一句話說的好:“多和舊人做新事”,我想我和 json 大概也屬于這種關(guān)系吧(?)

json 解析時(shí)字段名稱保持一致

這個(gè)疑問是,假如我們編碼不太規(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ì)將程序中定義的字段名稱改成駝峰或者什么特殊大小寫規(guī)則,而是完完全全使用原本的字符。如果是我目前的這個(gè)需求,即僅用來保存數(shù)據(jù),編碼和解碼都在后端進(jìn)行,那這樣完全可用不需要考慮更多,但如果是需要前后端數(shù)據(jù)對(duì)齊,而且有特殊的字段名稱規(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ì)于解碼來說,是大小寫不敏感的,就算傳過來的是某種形式的妖魔鬼怪也可以解析出來,比如

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)體里定義名稱相同的字段,即便有大小寫的區(qū)別,也會(huì)導(dǎo)致不可預(yù)料的情況發(fā)生。而且嚴(yán)格按照駝峰格式命名的話,不存在大小寫區(qū)別,相同字母的字段就是唯一的。

而 Go 團(tuán)隊(duì)也將在 json/v2 中默認(rèn)大小寫敏感,規(guī)范的行為肯定會(huì)帶來更少的 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)體類型不是問題,就可以使用原結(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 就不進(jìn)行編碼,沒有 omitempty 會(huì)編碼成 null。
  • 經(jīng)過測(cè)試不僅是指針類型的結(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 測(cè)試結(jié)構(gòu)體類型和指針類型的性能。結(jié)論是在 CPU 性能上兩者差不多,但是一個(gè)指針類型的字段會(huì)多進(jìn)行一次內(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 編解碼方式

可以實(shí)現(xiàn) json 規(guī)定的接口,使結(jié)構(gòu)體執(zhí)行特定的編解碼方式,假設(shè)下面一種情況,我希望業(yè)務(wù)代碼開發(fā)中使用方便查詢和操作的map,然后存儲(chǔ)或者通訊使用占用空間更少的數(shù)組或者切片,但同時(shí)我又不想增加開發(fā)人員的心智負(fù)擔(dān),想要之前怎么使用現(xiàn)在就如何使用,或者無法更改一些庫(kù)的執(zhí)行方式只能繞路。也就是說平時(shí)開發(fā)時(shí)需要直接調(diào)用 json.Marshal 或 json.UnMarshal,而不需要額外操作,這時(shí)就可以通過實(shí)現(xiàn)接口的方式達(dá)成目的,見如下代碼。

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 用指針接收器沒問題,因?yàn)樾枰薷恼{(diào)用這個(gè)方法的結(jié)構(gòu)體的字段值,但是 MarshalJSON 盡量用值接收器,因?yàn)檫@樣在調(diào)用 json.Marshal 時(shí)無論傳入的是值還是指針都能正常編碼,同時(shí)也避免了傳入的是 nil 導(dǎo)致 panic。

被遺忘在角落的 gob

在 golang 源碼的 encoding 包下有很多編解碼方式,比如 json、xml、base64 等等,但其中也有一個(gè) gob,假如你之前沒有接觸過 golang 這門編程語言那你大概率沒有聽說過這種編碼解碼方式,因?yàn)樗酮?dú)屬于 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 編碼多個(gè)結(jié)構(gòu)體,然后連續(xù)解碼多個(gè)結(jié)構(gòu)體。此外和 json 一樣也可以實(shí)現(xiàn)特定的接口來自定義編解碼行為,具體可以參考https://pkg.go.dev/encoding/gob。

向 json 和 xml 這種編碼方式方便讓我們?nèi)庋塾^察,但因此也犧牲了性能和空間,而 gob 類似 protobuf 都是生成二進(jìn)制,但是 gob 僅存在于 golang 生態(tài)中,普及度遠(yuǎn)遠(yuǎn)不及可以生成多種語言代碼的 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 性能上并沒有看到什么差距,但是內(nèi)存分配差了蠻多,如果不考慮通用性和擴(kuò)展性的話,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 持有的信息用來 debug 和測(cè)試,這個(gè)時(shí)候因?yàn)槭遣魂P(guān)鍵的 debug 信息所以也就懶得定義消息結(jié)構(gòu)體,而是直接在B中用 json 將數(shù)據(jù)序列化成字符串傳給 A,然后 A 在外面封裝一層錯(cuò)誤碼和數(shù)據(jù)傳給前端,如果直接這么操作會(huì)有一個(gè)問題:

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 時(shí),其中的雙引號(hào)會(huì)被轉(zhuǎn)義,甚至于三層四層來回傳遞后轉(zhuǎn)移符號(hào)會(huì)越來越多。所以這個(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ā)送消息,然后消息有不同的類型,每個(gè)類型的內(nèi)容的結(jié)構(gòu)都不一樣,這時(shí)需要先解碼通用結(jié)構(gòu),然后拿到消息類型,再根據(jù)消息類型解碼具體消息內(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)下來看是有些老舊的,由于 Go 的兼容性承諾也不便對(duì)其進(jìn)行大刀闊斧的改動(dòng),正是因?yàn)槿绱?,在最近的版本?go 團(tuán)隊(duì)推出了新的 json 包也就是 json/v2 來解決 json 編解碼的一些痛點(diǎ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 提供出來以保證兼容性,這些選項(xiàng)不乏上文提到的一些特殊性質(zhì),譬如:

  • 編解碼結(jié)構(gòu)體時(shí)字段大小寫敏感 (case-sensitive)
  • omitempty 起作用的對(duì)象會(huì)發(fā)生變化
  • nil 的 slice 和 map 會(huì)編碼成空數(shù)組和空結(jié)構(gòu)體而不是 null
  • 以及其他的一些性質(zhì)

當(dāng)然不只是一些編解碼行為發(fā)生了變化,性能方面也有了很大提高,甚至還能看到專門的文章介紹和分析當(dāng)前社區(qū)流行的諸多 json 庫(kù)和 json/v2 的對(duì)比,老熟人 sonic 也在其中,具體內(nèi)容詳見 [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)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言庫(kù)系列之dotsql詳解

    Go語言庫(kù)系列之dotsql詳解

    這篇文章主要介紹了Go語言庫(kù)系列之dotsql的相關(guān)知識(shí),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-04-04
  • golang 后臺(tái)進(jìn)程的啟動(dòng)和停止操作

    golang 后臺(tái)進(jìn)程的啟動(dòng)和停止操作

    這篇文章主要介紹了golang 后臺(tái)進(jìn)程的啟動(dòng)和停止操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Golang中的sync.WaitGroup用法實(shí)例

    Golang中的sync.WaitGroup用法實(shí)例

    這篇文章主要介紹了Golang中的sync.WaitGroup用法實(shí)例,WaitGroup的用途,它能夠一直等到所有的goroutine執(zhí)行完成,并且阻塞主線程的執(zhí)行,直到所有的goroutine執(zhí)行完成,需要的朋友可以參考下
    2015-07-07
  • GoLand如何設(shè)置中文

    GoLand如何設(shè)置中文

    這篇文章主要介紹了GoLand如何設(shè)置中文,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-12-12
  • go語言數(shù)組及結(jié)構(gòu)體繼承和初始化示例解析

    go語言數(shù)組及結(jié)構(gòu)體繼承和初始化示例解析

    這篇文章主要為大家介紹了go語言數(shù)組及結(jié)構(gòu)體繼承和初始化示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2022-04-04
  • Go REFLECT Library反射類型詳解

    Go REFLECT Library反射類型詳解

    這篇文章主要為大家介紹了Go REFLECT Library反射類型詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • Go語言中使用Swagger 生成 API 文檔及常見問題解決

    Go語言中使用Swagger 生成 API 文檔及常見問題解決

    Swagger 是一個(gè)規(guī)范和完整的框架,用于生成、描述、調(diào)用和可視化 RESTful風(fēng)格的Web服務(wù),本文就來詳細(xì)的介紹一下如何實(shí)現(xiàn)Go語言中使用Swagger 生成 API 文檔,感興趣的可以了解一下
    2026-01-01
  • go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟

    go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟

    這篇文章主要為大家介紹了go通過編碼縮短字符串的長(zhǎng)度實(shí)現(xiàn)方法步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • 深入了解Golang的map增量擴(kuò)容

    深入了解Golang的map增量擴(kuò)容

    這篇文章主要介紹了深入了解Golang的map增量擴(kuò)容,擴(kuò)容的主要目的是為了縮短map容器的響應(yīng)時(shí)間。增量擴(kuò)容的本質(zhì)其實(shí)就是將總的擴(kuò)容時(shí)間分?jǐn)偟搅嗣恳淮蝖ash操作上,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下
    2022-06-06
  • 利用Go語言實(shí)現(xiàn)在終端繪制小兔子

    利用Go語言實(shí)現(xiàn)在終端繪制小兔子

    這篇文章主要為大家詳細(xì)介紹了如何利用Go語言實(shí)現(xiàn)在終端繪制小兔子來給大家拜個(gè)早年,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2023-01-01

最新評(píng)論

香港| 资阳市| 健康| 东至县| 永定县| 治县。| 彝良县| 资中县| 托里县| 关岭| 邹平县| 会理县| 澄江县| 南阳市| 睢宁县| 大化| 三穗县| 尚志市| 达州市| 五家渠市| 河间市| 邹城市| 交口县| 庆云县| 赣州市| 六枝特区| 苏尼特左旗| 福鼎市| 西充县| 上思县| 林州市| 隆回县| 长汀县| 新郑市| 织金县| 堆龙德庆县| 社旗县| 故城县| 石景山区| 达拉特旗| 抚远县|