$ go get github.com/bytedance/sonic

基本使用

sonic提供了許多功能。本文僅列舉其中較為有特色的功能。感興趣的同學(xué)可以去看一下官方的examples

序列化/反序列化

sonic的使用類似于標(biāo)準(zhǔn)包encoding/json包的使用.

func base() {
m := map[string]interface{}{
"name": "z3",
"age": 20,
}

// sonic序列化
byt, err := sonic.Marshal(&m)
if err != nil {
log.Println(err)
}
fmt.Printf("json: %+v\n", string(byt))

// sonic反序列化
um := make(map[string]interface{})
err = sonic.Unmarshal(byt, &um)
if err != nil {
log.Println(err)
}
fmt.Printf("unjson: %+v\n", um)
}

// print
// json: {"name":"z3","age":20}
// unjson: map[age:20 name:z3]

sonic還支持流式的輸入輸出

Sonic 支持解碼?io.Reader?中輸入的 json,或?qū)ο缶幋a為 json 后輸出至?io.Writer,以處理多個值并減少內(nèi)存消耗

func base() {
m := map[string]interface{}{
"name": "z3",
"age": 20,
}

// 流式io編解碼
// 編碼
var encbuf bytes.Buffer
enc := sonic.ConfigDefault.NewEncoder(&encbuf)
if err := enc.Encode(m); err != nil {
log.Fatal(err)
} else {
fmt.Printf("cutomize encoder: %+v", encbuf.String())
}

// 解碼
var decbuf bytes.Buffer
decbuf.WriteString(encbuf.String())
clear(m)

dec := sonic.ConfigDefault.NewDecoder(&decbuf)
if err := dec.Decode(&m); err != nil {
log.Fatal(err)
} else {
fmt.Printf("cutomize decoder: %+v\n", m)
}
}

// print
// cutomize encoder: {"name":"z3","age":20}
// cutomize decoder: map[age:20 name:z3]

配置

在上面的自定義流式編碼解碼器,細(xì)心的朋友可能看到我們創(chuàng)建編碼器和解碼器的時候,是通過sonic.ConfigDefault.NewEncoder() / sonic.ConfigDefault.NewDecoder()這兩個函數(shù)進(jìn)行調(diào)用的。那么sonic.ConfigDefault是什么?

我們可以通過查看源碼:

var (
// ConfigDefault is the default config of APIs, aiming at efficiency and safty.
// ConfigDefault api的默認(rèn)配置,針對效率和安全。
ConfigDefault = Config{}.Froze()

// ConfigStd is the standard config of APIs, aiming at being compatible with encoding/json.
// ConfigStd是api的標(biāo)準(zhǔn)配置,旨在與encoding/json兼容。
ConfigStd = Config{
EscapeHTML : true,
SortMapKeys: true,
CompactMarshaler: true,
CopyString : true,
ValidateString : true,
}.Froze()

// ConfigFastest is the fastest config of APIs, aiming at speed.
// ConfigFastest是api的最快配置,旨在提高速度。
ConfigFastest = Config{
NoQuoteTextMarshaler: true,
}.Froze()
)

sonic提供了三種常用的Config配置。這些配置中對一些場景已經(jīng)預(yù)定義好了對應(yīng)的Config。

其實我們使用的sonic.Marshal()函數(shù)就是調(diào)用了默認(rèn)的ConfigDefault

 // Marshal returns the JSON encoding bytes of v.
func Marshal(val interface{}) ([]byte, error) {
return ConfigDefault.Marshal(val)
}

// Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.
// NOTICE: This API copies given buffer by default,
// if you want to pass JSON more efficiently, use UnmarshalString instead.
func Unmarshal(buf []byte, val interface{}) error {
return ConfigDefault.Unmarshal(buf, val)
}

但是在一些場景下我們不滿足于sonic預(yù)定義的三個Config。此時我們可以定義自己的Config進(jìn)行個性化的編碼和解碼。

首先先看一下Config的結(jié)構(gòu)。

 // Config is a combination of sonic/encoder.Options and sonic/decoder.Options
type Config struct {
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML bool
// SortMapKeys indicates encoder that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys bool
// CompactMarshaler indicates encoder that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler bool
// NoQuoteTextMarshaler indicates encoder that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler bool
// NoNullSliceOrMap indicates encoder that all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap bool
// UseInt64 indicates decoder to unmarshal an integer into an interface{} as an
// int64 instead of as a float64.
UseInt64 bool
// UseNumber indicates decoder to unmarshal a number into an interface{} as a
// json.Number instead of as a float64.
UseNumber bool
// UseUnicodeErrors indicates decoder to return an error when encounter invalid
// UTF-8 escape sequences.
UseUnicodeErrors bool
// DisallowUnknownFields indicates decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
DisallowUnknownFields bool
// CopyString indicates decoder to decode string values by copying instead of referring.
CopyString bool
// ValidateString indicates decoder and encoder to valid string values: decoder will return errors
// when unescaped control chars(\u0000-\u001f) in the string value of JSON.
ValidateString bool
}

由于字段較多。筆者就選擇幾個字段進(jìn)行演示,其他字段使用方式都是一致。

假設(shè)我們希望對JSON序列化按照key進(jìn)行排序以及將JSON編碼成緊湊的格式。我們可以配置Config進(jìn)行Marshal操作

func base() {
snc := sonic.Config{
CompactMarshaler: true,
SortMapKeys: true,
}.Froze()

snc.Marshal(obj)
}

考慮到排序帶來的性能損失(約 10% ), sonic 默認(rèn)不會啟用這個功能。

Sonic 默認(rèn)將基本類型(?struct?,?map?等)編碼為緊湊格式的 JSON ,除非使用?json.RawMessage?or?json.Marshaler?進(jìn)行編碼:sonic 確保輸出的 JSON 合法,但出于性能考慮,不會加工成緊湊格式。我們提供選項?encoder.CompactMarshaler?來添加此過程,

Ast.Node

sonic提供了Ast.Node的功能。Sonic/ast.Node 是完全獨立的 JSON 抽象語法樹庫。它實現(xiàn)了序列化和反序列化,并提供了獲取和修改通用數(shù)據(jù)的魯棒的 API

先來簡單介紹一下Ast.Node:ast.Node 通常指的是編程語言中的抽象語法樹(Abstract Syntax Tree)節(jié)點。抽象語法樹是編程語言代碼在編譯器中的內(nèi)部表示,它以樹狀結(jié)構(gòu)展現(xiàn)代碼的語法結(jié)構(gòu),便于編譯器進(jìn)行語法分析、語義分析、優(yōu)化等操作。

在很多編程語言的編譯器或解釋器實現(xiàn)中,抽象語法樹中的每個元素(節(jié)點)都會有對應(yīng)的數(shù)據(jù)結(jié)構(gòu)表示,通常這些數(shù)據(jù)結(jié)構(gòu)會被稱為?ast.Node?或類似的名字。每個?ast.Node?表示源代碼中的一個語法結(jié)構(gòu),如表達(dá)式、語句、函數(shù)聲明等。

抽象語法樹的節(jié)點可以包含以下信息:

我們通過幾個案例理解一下Ast.Node的使用。

準(zhǔn)備數(shù)據(jù)

data := {"name": "z3","info":{"num": [11,22,33]}}

將數(shù)據(jù)轉(zhuǎn)換為Ast.Node

通過傳入bytes或者string返回一個Ast.Node。其中你可以指定path獲取JSON中的子路徑元素。

每個路徑參數(shù)必須是整數(shù)或者字符串

 // 函數(shù)簽名:
func Get(src []byte, path ...interface{}) (ast.Node, error) {
return GetFromString(string(src), path...)
}
// GetFromString與Get相同,只是src是字符串,這樣可以減少不必要的內(nèi)存拷貝。
func GetFromString(src string, path ...interface{}) (ast.Node, error) {
return ast.NewSearcher(src).GetByPath(path...)
}

獲取當(dāng)前節(jié)點的完整數(shù)據(jù)

func base() {
data := {"name": "z3","info":{"num": [11,22,33]}} // no path return all json string root, err := sonic.GetFromString(data) if err != nil { log.Panic(err) } if raw, err := root.Raw(); err != nil { log.Panic(err) } else { log.Println(raw) } } // print // 2023/08/26 17:15:52 {"name": "z3","info":{"num": [11,22,33]}}

根據(jù)path或者索引獲取數(shù)據(jù)

 func base() {
data := {"name": "z3","info":{"num": [11,22,33]}} // no path return all json string root, err := sonic.GetFromString(data) if err != nil { log.Panic(err) } // according to path(根據(jù)key,查詢當(dāng)前node下的元素) if path, err := root.GetByPath("name").Raw(); err != nil { log.Panic(err) } else { log.Println(path) } // indexOrget (同時提供index和key進(jìn)行索引和key的匹配) if path, err := root.IndexOrGet(1, "info").Raw(); err != nil { log.Panic(err) } else { log.Println(path) } // index (按照index進(jìn)行查找當(dāng)前node下的元素) // root.Index(1).Index(0).Raw()意味著 // root.Index(1) == "info" // root.Index(1).Index(0) == "num" // root.Index(1).Index(0).Raw() == "[11,22,33]" if path, err := root.Index(1).Index(0).Raw(); err != nil { log.Panic(err) } else { log.Println(path) } } // print // 2023/08/26 17:17:49 "z3" // 2023/08/26 17:17:49 {"num": [11,22,33]} // 2023/08/26 17:17:49 [11,22,33]

Ast.Node支持鏈?zhǔn)秸{(diào)用。故我們可以從root node節(jié)點,根據(jù)path路徑向下搜索指定的元素。

index和key混用

user := root.GetByPath("statuses", 3, "user")  // === root.Get("status").Index(3).Get("user")

根據(jù)path進(jìn)行修改數(shù)據(jù)

 func base() {
data := {"name": "z3","info":{"num": [11,22,33]}} // no path return all json string root, err := sonic.GetFromString(data) if err != nil { log.Panic(err) } // according to path if path, err := root.GetByPath("name").Raw(); err != nil { log.Panic(err) } else { log.Println(path) } // indexOrget (同時提供index和key進(jìn)行索引和key的匹配) if path, err := root.IndexOrGet(1, "info").Raw(); err != nil { log.Panic(err) } else { log.Println(path) } // index if path, err := root.Index(1).Index(0).Raw(); err != nil { log.Panic(err) } else { log.Println(path) } // set // ast提供了很多go類型轉(zhuǎn)換node的函數(shù) if _, err := root.Index(1).SetByIndex(0, ast.NewArray([]ast.Node{ ast.NewNumber("101"), ast.NewNumber("202"), })); err != nil { log.Panic(err) } raw, _ := root.Raw() log.Println(raw) } // print // 2023/08/26 17:23:55 "z3" // 2023/08/26 17:23:55 {"num": [11,22,33]} // 2023/08/26 17:23:55 [11,22,33] // 2023/08/26 17:23:55 {"name":"z3","info":{"num":[101,202]}}

序列化

func base() {

data := {"name": "z3","info":{"num": [11,22,33]}} // no path return all json string root, err := sonic.GetFromString(data) if err != nil { log.Panic(err) } bts, _ := root.MarshalJSON() log.Println("Ast.Node(Marshal): ", string(bts)) btes, _ := json.Marshal(&root) log.Println("encoding/json (Marshal): ", string(btes)) } // print // 2023/08/26 17:39:06 Ast.Node(Marshal): {"name": "z3","info":{"num": [11,22,33]}} // 2023/08/26 17:39:06 encoding/json (Marshal): {"name":"z3","info":{"num":[11,22,33]}}

?: 使用json.Marshal() (必須傳遞指向節(jié)點的指針)

API

Ast.Node提供了許多有特色的API,感興趣的朋友可以去試一下。

最佳實踐

預(yù)熱

由于 Sonic 使用  golang-asm (https://github.com/twitchyliquid64/golang-asm) 作為 JIT 匯編器,這個庫并不適用于運行時編譯,第一次運行一個大型模式可能會導(dǎo)致請求超時甚至進(jìn)程內(nèi)存溢出。為了更好地穩(wěn)定性,我們建議在運行大型模式或在內(nèi)存有限的應(yīng)用中,在使用 Marshal()/Unmarshal() 前運行 Pretouch()

拷貝字符串

當(dāng)解碼?沒有轉(zhuǎn)義字符的字符串時, sonic 會從原始的 JSON 緩沖區(qū)內(nèi)引用而不是復(fù)制到新的一個緩沖區(qū)中。這對 CPU 的性能方面很有幫助,但是可能因此在解碼后對象仍在使用的時候?qū)⒄麄€ JSON 緩沖區(qū)保留在內(nèi)存中。實踐中我們發(fā)現(xiàn),通過引用 JSON 緩沖區(qū)引入的額外內(nèi)存通常是解碼后對象的 20% 至 80% ,一旦應(yīng)用長期保留這些對象(如緩存以備重用),服務(wù)器所使用的內(nèi)存可能會增加。我們提供了選項?decoder.CopyString()?供用戶選擇,不引用 JSON 緩沖區(qū)。這可能在一定程度上降低 CPU 性能

func base() {
// 在sonic.Config中進(jìn)行配置
snc := sonic.Config{
CopyString: true,
}.Froze()
}

傳遞字符串還是字節(jié)數(shù)組

為了和?encoding/json?保持一致,我們提供了傳遞?[]byte?作為參數(shù)的 API ,但考慮到安全性,字符串到字節(jié)的復(fù)制是同時進(jìn)行的,這在原始 JSON 非常大時可能會導(dǎo)致性能損失。因此,你可以使用?UnmarshalString()?和?GetFromString()?來傳遞字符串,只要你的原始數(shù)據(jù)是字符串,或零拷貝類型轉(zhuǎn)換對于你的字節(jié)數(shù)組是安全的。我們也提供了?MarshalString()?的 API ,以便對編碼的 JSON 字節(jié)數(shù)組進(jìn)行零拷貝類型轉(zhuǎn)換,因為 sonic 輸出的字節(jié)始終是重復(fù)并且唯一的,所以這樣是安全的。

零拷貝類型轉(zhuǎn)換是一種技術(shù),它允許你在不進(jìn)行實際數(shù)據(jù)復(fù)制的情況下,將一種數(shù)據(jù)類型轉(zhuǎn)換為另一種數(shù)據(jù)類型。這種轉(zhuǎn)換通過操作原始內(nèi)存塊的指針和切片來實現(xiàn),避免了額外的數(shù)據(jù)復(fù)制,從而提高性能并減少內(nèi)存開銷.

需要注意的是,零拷貝類型轉(zhuǎn)換雖然可以提高性能,但也可能引入一些安全和可維護(hù)性的問題,特別是當(dāng)直接操作指針或內(nèi)存映射時。

性能優(yōu)化

在?完全解析的場景下,?Unmarshal()?表現(xiàn)得比?Get()+Node.Interface()?更好。

func base() {
data := {"name": "z3","info":{"num": [11,22,33]}} // complete parsing m := map[string]interface{}{} sonic.Unmarshal([]byte(data), &m) }

但是如果你只有特定 JSON的部分模式,你可以將?Get()?和?Unmarshal()?結(jié)合使用:

func base() {
data := {"name": "z3","info":{"num": [11,22,33]}} // complete parsing m := map[string]interface{}{} sonic.Unmarshal([]byte(data), &m) // partial parsing clear(m) node, err := sonic.GetFromString(data, "info", "num", 1) if err != nil { panic(err) } log.Println(node.Raw()) }

本文章轉(zhuǎn)載微信公眾號@Go開發(fā)大全

上一篇:

NLP文本分類任務(wù)實戰(zhàn),附代碼模板,手把手帶你跑通

下一篇:

基于 Gin 從0到1搭建 Web 管理后臺系統(tǒng)后端服務(wù)(一)項目初始化、配置和日志
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費