探究|Go JSON 三方包哪家強?

本文作者從評判標準、功能評測、性能評測等多方面剖析三方庫哪些家強,並給出了比較務實的建議。

引言

爲了小夥伴理解,彙總了一下文章中會提及的術語名詞解釋,請放心品讀,歡迎一起討論交流!

圖片

 

你真的瞭解 Go 標準庫嗎?

問題一:標準庫可以反序列化普通的字符串嗎?執行下面的代碼會報錯嗎?


var s string
err := json.Unmarshal([]byte(`"Hello, world!"`), &s)
assert.NoError(t, err)
fmt.Println(s)
// 輸出:
// Hello, world!

解:其實標準庫解析不僅支持是對象、數組,同時也可以是字符串、數值、布爾值以及空值,但需要注意,上面字符串中的雙引號不能缺,否則將不是一個合法的 json 序列,會返回錯誤。

問題二:如果結構體的 json tag 定義與 key 大小不一致,可以反序列化成功嗎?

cert := struct {
    Username string `json:"username"`
    Password string `json:"password"`
}{}
err = json.Unmarshal([]byte(`{"UserName":"root","passWord":"123456"}`), &cert)
if err != nil {
    fmt.Println("err =", err)
} else {
    fmt.Println("username =", cert.Username)
    fmt.Println("password =", cert.Password)
}
// 輸出:
// username = root
// password = 123456

解:如果遇到大小寫問題,標準庫會盡可能地進行大小寫轉換,即:一個 key 與結構體中的定義不同,但忽略大小寫後是相同的,那麼依然能夠爲字段賦值。

爲什麼使用第三方庫,標準庫有哪些不足?

Go json 標準庫 encoding/json[1]已經是提供了足夠舒適的 json 處理工具,廣受 Go 開發者的好評,但還是存在以下兩點問題:
  • API 不夠靈活:如沒有提供按需加載機制等;

  • 性能不太高:標準庫大量使用反射獲取值,首先 Go 的反射本身性能較差,較耗費 CPU 配置;其次頻繁分配對象,也會帶來內存分配和 GC 的開銷;
基於上面的考量,業務會根據使用場景、降本收益等訴求,引入合適的第三方庫。

三方庫哪些家強?

首先,思考從提問開始:
  • 熱門的三方庫有哪些?
  • 內部實現原理是什麼?
  • 如何結合業務去選型?

下面是我收集整理的一些開源第三方庫,也歡迎有興趣的小夥伴一起交流補充!
庫名 encoder decoder compatible star 數 (2023.04.19) 社區維護性
StdLib(encoding/json)[2] ✔️ ✔️ N/A - -
FastJson(valyala/fastjson)[3] ✔️ ✔️ 1.9k 較差
GJson(tidwall/gjson)[4] ✔️ ✔️ 12.1k 較好
JsonParser(buger/jsonparser)[5] ✔️ ✔️ 5k 較差
JsonIter(json-iterator/go)[6] ✔️ ✔️ 部分兼容 12.1k 較差
GoJson(goccy/go-json)[7] ✔️ ✔️ ✔️ 2.2k 較好
EasyJson(mailru/easyjson)[8] ✔️ ✔️ 4.1k 較差
Sonic(bytedance/sonic)[9] ✔️ ✔️ ✔️ 4.1k 較好

評判標準

評判標準包含三個維度:

  • 性能:內部實現原理是什麼,是否使用反射機制;

  • 穩定性:考慮到要投入生產使用,必須是一個較爲穩定的三方庫;

  • 功能靈活性:是否支持 Unmarshal 到 map 或 struct,是否提供的一些定製化抽取的 API;
在功能劃分上,根據主流 json 庫 API,將它們的使用方式分爲三種:
  • 泛型(generic)編解碼:json 沒有對應的 schema,只能依據自描述語義將讀取到的 value 解釋爲對應語言的運行時對象,例如:json object 轉化爲 Go map[string]interface{};

  • 定型(binding)編解碼:json 有對應的 schema,可以同時結合模型定義(Go struct)與 json 語法,將讀取到的 value 綁定到對應的模型字段上去,同時完成數據解析與校驗;

  • 查找(get)& 修改(set):指定某種規則的查找路徑(一般是 key 與 index 的集合),獲取需要的那部分 json value 並處理。

功能評測

圖片

特點分析

圖片

性能評測

下面是評測性能時所用的各個包的版本情況,具體的測試代碼,可參考benchmark_test [10]。

圖片

爲什麼沒有評測 GJson 庫呢?
GJson 在單鍵查找的場景下有很大的優勢。這是因爲它的查找是通過 lazy-load 實現的,它巧妙地跳過了傳遞值,有效地減少了很多不必要的解析,但其實跳過也是一種輕量級解析,實際是在處理 json 控制字符“[”、“{”等;然而當涉及到多鍵查找時,Gjson 做的事比標準庫更糟糕,這是其跳過機制的副作用:相同路徑查找導致的重複開銷。針對它的使用場景,單純的評測定性編解碼和泛型編解碼,對 GJson 是不公平的。
根據樣本 json 的 key 數量和深度分爲三個量級:
  • Small[11](400B, 11 keys, 3 layers)
  • Medium[12](13KB, 300+ key, 6 layers)
  • Large[13](635KB, 10000+ key, 6 layers)

評測過程中,需要注意不但要區分泛型編/解碼、定型編/解碼,而且也要考慮到併發情況下的性能表現,測試代碼樣例如下:
func BenchmarkEncoder_Generic_StdLib(b *testing.B) {
    _, _ = json.Marshal(_GenericValue)
    b.SetBytes(int64(len(TwitterJson)))
    b.ResetTimer()
for i := 0; i < b.N; i++ {
        _, _ = json.Marshal(_GenericValue)
    }
}
func BenchmarkEncoder_Binding_StdLib(b *testing.B) {
    _, _ = json.Marshal(&_BindingValue)
    b.SetBytes(int64(len(TwitterJson)))
    b.ResetTimer()
for i := 0; i < b.N; i++ {
        _, _ = json.Marshal(&_BindingValue)
    }
}
func BenchmarkEncoder_Parallel_Generic_StdLib(b *testing.B) {
    _, _ = json.Marshal(_GenericValue)
    b.SetBytes(int64(len(TwitterJson)))
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
            _, _ = json.Marshal(_GenericValue)
        }
    })
}
func BenchmarkEncoder_Parallel_Binding_StdLib(b *testing.B) {
    _, _ = json.Marshal(&_BindingValue)
    b.SetBytes(int64(len(TwitterJson)))
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
            _, _ = json.Marshal(&_BindingValue)
        }
    })
}

具體的指標數據和統計結果,可參考benchmark_readme [14],總體結論如下:

圖片


常見優化思路有哪些?

定型編解碼

對於有 schema 的定型編解碼場景而言,很多運算其實不需要在“運行時”執行。例如業務模型中確定了某個 json key 的值一定是布爾類型,其實可以在序列化階段直接輸出這個對象對應的 json 值(true/false),並不需要再檢查這個對象的具體類型。其核心思想是優化數據處理邏輯,將模型解釋與數據處理邏輯分離,讓前者提前固定下來,進而消除反射,提升性能。

動態函數組裝

JsonIter 就使用函數組裝模式,具體方法是將結構解釋爲逐個字段的編碼和解碼函數,然後將其組裝並緩存爲整個對象對應的編解碼器,運行時再加載出來處理 json,將反射的性能損失成本降到最低。
這樣優化是否就能一勞永逸呢?
並沒有。因爲這樣實現會轉化爲大量的接口封裝和函數調用棧。在實際測試中,會發現隨着 json 數據量級的增長,因爲調用接口涉及動態尋址,彙編函數不能被內聯,而 Golang 的函數調用性能很差(沒有逐個寄存器的參數傳遞),函數調用的開銷也會成倍放大。

減少函數調用

爲了避免動態彙編的函數調用開銷,業界實現方式目前主要有兩種:code-gen(代碼生成 ) 和 JIT( 即時編譯)。

code-gen

EasyJson 就採用代碼生成的思路。這種實現方式庫開發者實現起來相對簡單,性能高;但是它伴隨着模式依賴性和便利性的損失,增加業務代碼的維護成本和侷限性,無法做到秒級熱更新,這也是代碼生成方式的 json 庫受衆並不廣泛的原因。

JIT

即時編譯將編譯過程移到了程序的加載或首次解析階段,只需要提供 json schema 對應的結構體類型信息,就可以一次性編譯生成對應的編解碼器,通常以 Golang 函數的形式緩存到堆外內存,便於後期高效執行。
// 函數緩存
type cache struct {
  functions map[*rtype]function
lock      sync.Mutex
}
var (
global = func() [caches]*cache {
var caches [caches]*cache
for idx := range caches {
      caches[idx] = &cache{functions: make(map[*rtype]function, 4)}
    }
return caches
  }()
)
func load(typ *rtype) (function, bool) {
do, ok := global[uintptr(unsafe.Pointer(typ))%caches].functions[typ]
return do, ok
}
func save(typ *rtype, do function) {
cache := global[uintptr(unsafe.Pointer(typ))%caches]
  cache.lock.Lock()
  cache.functions[typ] = do
  cache.lock.Unlock()
}

泛型編解碼

其實泛型編解碼性能差不只是因爲沒有 schema ,因爲可以對比一下 C++ 的 json 庫,如 simdjson[15],它的解析方式都是泛型的,但性能仍然很好;
Go 標準庫泛型解析性能差,原因在於它採用了 map[string]interface{} 作爲 json 的編解碼對象。這其實是一種糟糕的選擇,原因如下:
  • 數據反序列化的過程中,map 插入的開銷很高;
  • 在數據序列化過程中,map 遍歷也遠不如數組高效;

如果用一種與 json AST 更貼近的數據結構來描述,不但可以讓轉換過程更加簡單,甚至可以實現 lazy-load 。

複用編碼緩衝區

通過使用 sync.Pool 複用先前編碼的緩衝區,可以有效減少 encode buffer 的內存分配次數。

type buffer struct {
    data []byte
}
var bufPool = sync.Pool{
    New: func() interface{} {
return &buffer{data: make([]byte, 0, 1024)}
    },
}
// 複用緩衝區
buf := bufPool.Get().(*buffer)
data := encode(buf.data)
newBuf := make([]byte, len(data))
copy(newBuf, buf)
buf.data = data
bufPool.Put(buf)

Sonic 庫爲什麼性能好?

原理調研

Sonic 基於彙編進行開發,通過充分利用向量化(SIMD)指令、優化內存佈局和按需解析等關鍵技術,大幅提高了序列化反序列化性能。
它的優化思路可以分成離線和在線:
  • 離線場景:針對 Go 語言編譯優化的不足,Sonic 核心計算函數使用 C 語言編寫,使用 Clang 的深度優化編譯選項,並開發了一套 asm2asm 工具,將完全優化的 x86 彙編翻譯成 plan9 彙編,加載到 Golang 運行時,以供調用。

  • 在線場景:通過自定義 AST,實現了按需加載;採用 JIT 技術在運行時對模式對應的操作碼進行裝配,以 Golang 函數的形式緩存到堆外內存。這樣大大減少函數調用,同時也保證靈活性;

圖片

爲什麼不使用 CGO ?
雖然使用 CGO 實現更加簡便, 但 CGO 在調用 c 代碼的時候引入了調度、切換線程棧等開銷,會造成較大(有的場景中高達 20 多倍)的性能損耗,無法對代碼進行了深度優化。

SIMD

什麼是 SIMD?
SIMD(Single-Instruction-Multi-Data 單指令流多數據流)它是一種採用一個控制器來控制多個處理器,同時對一組數據中的每一個數據分別執行相同的操作,從而實現空間上的並行性技術。例如:X86 的 SSE 或者 AVX2 指令集,以及 ARM 的 NEON 指令集等。它作爲一組特殊的 CPU 指令,用於矢量數據的並行處理。目前,它被大多數 CPU 所支持,並廣泛用於圖像處理和大數據計算中,當然在 json 處理中也很有用。
SIMD 在 json 處理中解決了什麼問題?
對於 json 文本的處理與計算。其中一些問題在業界已經有比較成熟高效的解決方案,如浮點數轉字符串算法 Ryu,整數轉字符串的查表法等;針對一些問題邏輯相對簡單,但是可能會面對較大數量級的文本,如 json string 的 unquote\quote 處理、空白字符的跳過等,也需要某種技術手段來提升處理能力,而 SIMD 就是一種用於並行處理大規模數據的技術。simdjson-go [16]在大型 json 場景(>100KB)中非常有競爭力。然而,對於一些極小的或不規則的字符串,SIMD 所需的額外負載操作將導致性能下降。因此, 對於大數據和小數據並存的實際場景,採用預設條件判斷(字符串大小、浮動精度等),將 SIMD 和標量指令結合起來,以達到最佳適應性。

asm2asm

爲了提高執行效率,Sonic 中一些關鍵的計算函數是用 C 編寫的,用 Clang 編譯的;但由於 Clang 編譯出來的是 x86 彙編,而 Golang 編譯出來的是 plan9 彙編;那如何將優化後的彙編嵌入 Golang 中就變成了一個問題,因此爲了在 Golang 中調用 Clang 編譯出來的彙編,字節開發了一個內部工具 tools/asm2asm 將 x86 的彙編轉換爲 plan9。

JIT 彙編

Sonic 借鑑 JsonIter 的組裝各類型處理函數的實現,針對編解碼器動態裝配的函數調用開銷,採用 JIT 技術在運行時對模式對應的操作碼(asm)進行裝配,最後以 Golang 函數的形式緩存到堆外內存。因爲編譯後的編解碼函數是一個集成函數,可以大大減少函數調用,同時保證靈活性;對於 json 對應的結構體已知的服務場景,在線進行 JIT 彙編是比較耗時的,會導致首次請求耗時較高,也可以預先生成好彙編後的字節碼。

RCU cache

爲了提升 Codec Cache 的加載速度,每個結構體對應的序列化/反序列化字節碼可以緩存起來,後面直接調用,以減少運行時彙編操作的執行次數(緩存足夠大的時候只需要執行一次)。Sync.Map 最初是用來緩存編解碼器的,但對於準靜態(讀遠多於寫)、較少元素(通常不超過幾十個)的場景來說,其性能並不理想,所以用“開放尋址哈希+RCU 技術”重新實現了一個高性能和併發安全的緩存。

自定義 AST

針對泛型編解碼,基於 map 開銷較大的考慮,Sonic 實現了更符合 json 結構的樹形 AST;通過自定義的一種通用的泛型數據容器 sonic-ast 替代 Go interface,從而提升性能。

用 node {type, length, pointer} 表示任意一個 json 數據節點,並結合樹與數組結構描述節點之間的層級關係。針對部分解析,考慮到解析和跳過之間的巨大速度差距,將 lazy-load 機制到 AST 解析器中,以一種更加自適應和高效的方式來減少多鍵查詢的開銷。


type Node struct {
v int64
t types.ValueType
p unsafe.Pointer
}

如何實現部分解析?

sonic-ast 實現了一種有狀態、可伸縮的 json 解析過程。當使用者 get 某個 key 時,採用 skip 計算來輕量化跳過要獲取的 key 之前的 json 文本;對於該 key 之後的 json 節點,直接不做任何的解析處理;僅使用者真正需要的 key 才完全解析。

如何解決相同路徑查找重複開銷的問題?

在對於子節點 skip 處理過程增加了一個步驟,將跳過 json 的 key、起始位、結束位記錄下來,分配一個 Raw-JSON 類型的節點保存下來,這樣二次 skip 就可以直接基於節點的 offset 進行,這樣解決相同路徑查找導致的重複開銷的問題。同時 sonic-ast 支持了節點的更新、插入和序列化,甚至支持將任意 Go types 轉爲節點並保存下來。

圖片

函數調用優化

  • 無棧內存管理:自己維護變量棧(內存池),避免 Go 函數棧擴展。

  • 自動生成跳轉表,加速 generic decoding 的分支跳轉。

  • 使用寄存器傳參:儘量避免 memory load & store,將使用頻繁的變量放到固定的寄存器上,如:json buffer、結構體指針;

  • 重寫函數調用:由於彙編函數不能內聯到 Go 函數中,函數調用引入的開銷甚至會抵消 SIMD 帶來的性能提升,因此在 JIT 中重新實現了一組輕量級的函數調用(維護全局函數表+函數 offset)。

業務實踐

適用場景

由於 Sonic 優化的是 json 操作,所以在 json 操作的 cpu 開銷佔比較大的服務場景中收益會比較明顯。比如網關、轉發和入口服務等。

快速試用

爲了較小侵入地驗證 Sonic 會對服務產生的性能提升,評估是否值得切換。推薦使用brahma-adshonor/gohook [17]工具庫,內部大概實現是向被 hook 的函數地址中寫入跳轉指令,直接跳轉到新的函數地址。
使用方式:在 main 函數的入口處 hook 當前使用的 json 庫函數爲 Sonic 中對等函數。hook 是函數級的,因此可以具體驗證具體函數的性能提升;當人出於對某些函數的不信任、或者自己有性能更優異或更穩定的實現,也可以部分函數使用 Sonic。但需要注意,它未經過生產環境驗證,建議僅測試使用。切記,線上變更時,一定要滿足“可監控”,“可灰度”、“可回滾”的三大原則。

import "github.com/brahma-adshonor/gohook"
func main() {
// 在main函數的入口hook當前使用的json庫(如encoding/json)
    gohook.Hook(json.Marshal, sonic.Marshal, nil)
    gohook.Hook(json.Unmarshal, sonic.Unmarshal, nil)
}

收益情況

截止 2022 年 1 月份,Sonic 已應用於抖音,今日頭條等服務,累計爲字節節省了數十萬核。下圖爲字節某服務使用 Sonic 後高峯時段的 cpu 佔用核數對比(圖來源sonic :基於 JIT 技術的開源全場景高性能 JSON 庫)。在生產環境中,Sonic 中也驗證了良好的收益,服務高峯期佔用核數減少將近三分之一:

圖片

同時我們將線上的一個服務,將 HTTP 查詢接口 JSON 序列化由標準庫切換爲 Sonic 庫後,在相同 QPS 量級下,CPU 利用率下降了接近 3 個點,效果也還不錯。

圖片

使用事項

HTML Escape

標準庫中默認會開啓 html Escape,而 Sonic 出於性能損耗默認不開啓。

func TestEncode(t *testing.T) {
  data := map[string]string{"&&": "<>"}
// 標準庫
var w1 = bytes.NewBuffer(nil)
  enc1 := json.NewEncoder(w1)
  err := enc1.Encode(data)
  assert.NoError(t, err)
// Sonic 庫
var w2 = bytes.NewBuffer(nil)
  enc2 := encoder.NewStreamEncoder(w2)
  err = enc2.Encode(data)
  assert.NoError(t, err)
  fmt.Printf("%v%v", w1.String(), w2.String())
}
// 運行結果:
{"\u0026\u0026":"\u003c\u003e"}
{"&&":"<>"}

若有需要可以通過下面方式開啓:


import "github.com/bytedance/sonic/encoder"
v := map[string]string{"&&":"<>"}
ret, err := encoder.Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":" \u003e"}}`
enc := encoder.NewStreamEncoder(w)
enc.SetEscapeHTML(true)
err := enc.Encode(obj)

大型模式問題

由於 Sonic 使用 golang-asm 作爲 JIT 彙編器,這不太適合運行時編譯,因此首次運行大型模式可能會導致請求超時甚至處理 OOM。爲了獲得更好的穩定性,建議在 Marshal()/Unmarshal()之前使用 Pretouch()來處理大型模式或緊湊內存應用程序。

import (
"reflect"
"github.com/bytedance/sonic"
"github.com/bytedance/sonic/option"
)
func init() {
var v HugeStruct
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
    err := sonic.Pretouch(reflect.TypeOf(v))
// with more CompileOption...
    err := sonic.Pretouch(reflect.TypeOf(v),
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
// you can set compile recursive loops in Pretouch for better stability in JIT.
        option.WithCompileRecursiveDepth(loop),
// For a large nested struct, try to set a smaller depth to reduce compiling time.
        option.WithCompileMaxInlineDepth(depth),
    )
}

key 排序

Sonic 在序列化時默認是不對 key 進行排序的。json 的規範也與順序無關,但若需要 json 是有序的,可以在序列化時選擇排序的配置,大約會帶來 10%的性能損耗。排序方法如下:

import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()

暫不支持 arm 架構

現象:用 Mac M1 無法編譯成功,解決方案可參考 sonic-compatibility[19],在編譯時通過添加以下參數: GOARCH=amd64,可解決編譯失敗的問題,但依舊無法支持本地 Debug 操作;
官方在 issue172 [20]中曾表示因內部實現的原因,這個問題的確很難搞,但還是有望在 Sonic V2 大版本中支持。

總結

綜上,業務選型上需要根據具體情況、不同領域的業務使用場景和發展趨勢進行選擇,綜合考慮各方面因素。最適配業務的纔是最好的! 例如:如果業務只是簡單的解析 http 請求返回的 json 串的部分字段,並且字段都是確定的,偶爾需要搜索功能,那 Gjson 是很不錯的選擇。
以下是一些個人觀點,僅供參考:
  • 不太推薦使用 Jsoniter 庫,原因在於: Go 1.8 之前,官方 Json 庫的性能就收到多方詬病。不過隨着 Go 版本的迭代,標準 json 庫的性能也越來越高,Jsonter 的性能優勢也越來越窄。如果希望有極致的性能,應該選擇 Easyjson 等方案而不是 Jsoniter,而且 Jsoniter 近年已經不活躍了。

  • 比較推薦使用 Sonic 庫,因不論從性能和功能總體而言,Sonic 的表現的確很亮眼;此外,通過了解 Sonic 的內部實現原理,提供一種對於 cpu 密集型操作優化的“野路子”,即:通過編寫高性能的 C 代碼並經過優化編譯後供 Golang 直接調用。其實並不新鮮,因爲實際上 Go 源碼中的一些 cpu 密集型操作底層就是編譯成了彙編後使用的,如:crypto 和 math。

參考資料:

  • 深入 Go 中各個高性能 JSON 解析庫:https://www.luozhiyun.com/archives/535
  • Go 語言原生的 json 包有什麼問題?如何更好地處理 JSON 數據?:https://cloud.tencent.com/developer/article/1820473
  • go-json#how-it-works:https://github.com/goccy/go-json#how-it-works
  • sonic :基於 JIT 技術的開源全場景高性能 JSON 庫
  • Introduction to Sonic:https://github.com/bytedance/sonic/blob/main/INTRODUCTION.md
  • bytedance/sonic-readme:https://pkg.go.dev/github.com/bytedance/[email protected]#section-readme
  • 爲字節節省數十萬核的 json 庫 sonic:https://zhuanlan.zhihu.com/p/586050976

[1]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry

[2]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry

[3]https://github.com/valyala/fastjson?spm=ata.21736010.0.0.6e462b76wytEry

[4]https://github.com/tidwall/gjson?spm=ata.21736010.0.0.6e462b76wytEry

[5]https://github.com/buger/jsonparser?spm=ata.21736010.0.0.6e462b76wytEry

[6]https://github.com/json-iterator/go?spm=ata.21736010.0.0.6e462b76wytEry

[7]https://github.com/goccy/go-json?spm=ata.21736010.0.0.6e462b76wytEry

[8]https://github.com/mailru/easyjson?spm=ata.21736010.0.0.6e462b76wytEry

[9]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry

[10]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/bench.sh?spm=ata.21736010.0.0.6e462b76wytEry&file=bench.sh

[11]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/small.go?spm=ata.21736010.0.0.6e462b76wytEry&file=small.go

[12]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/medium.go?spm=ata.21736010.0.0.6e462b76wytEry&file=medium.go

[13]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/large.json?spm=ata.21736010.0.0.6e462b76wytEry&file=large.json

[14]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/README.md?spm=ata.21736010.0.0.6e462b76wytEry&file=README.md

[15]https://github.com/simdjson/simdjson?spm=ata.21736010.0.0.6e462b76wytEry

[16]https://github.com/minio/simdjson-go?spm=ata.21736010.0.0.6e462b76wytEry

[17]https://github.com/brahma-adshonor/gohook?spm=ata.21736010.0.0.6e462b76wytEry

[19]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry#compatibility

[20]https://github.com/bytedance/sonic/issues/172?spm=ata.21736010.0.0.6e462b76wytEry

 

作者|趙金鑫(筆話)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章