json-iterator 庫:https://github.com/json-iterator/go
動機
現有的golang解析json的庫都是push模式的,缺少一種基於pull api的庫。另外就是看一下golang解析json的速度到底如何,還有多少的提高空間。
API 風格
api 風格上是以 StAX 為基礎,但是針對 JSON 做了特別的優化。比 StAX 和 SAX 都更簡單可控。當然如果需要最簡單,還是 DOM 類的 api 最簡單。使用流式pull的api為的就是最大化控制解析過程。
解析 Array
iter := ParseString(`[1,2,3]`)
for iter.ReadArray() {
iter.ReadUint64()
}
可以看到,pull api 的風格非常不同。整個解析流程是呼叫者驅動的
解析 Object
type TestObj struct {
Field1 string
Field2 uint64
}
iter := ParseString(`{"field1": "1", "field2": 2}`)
obj := TestObj{}
for field := iter.ReadObject(); field != ""; field = iter.ReadObject() {
switch field {
case "field1":
obj.Field1 = iter.ReadString()
case "field2":
obj.Field2 = iter.ReadUint64()
default:
iter.ReportError("bind object", "unexpected field")
}
}
解析過程不依賴反射,解析出來的值幹什麼事情完全由你來操縱。你可以直接做一些累加操作,而不把值先繫結到物件上。
SKIP
iter := ParseString(`[ {"a" : [{"b": "c"}], "d": 102 }, "b"]`)
iter.ReadArray()
iter.Skip()
iter.ReadArray()
if iter.ReadString() != "b" {
t.FailNow()
}
對於不關心的欄位,可以選擇跳過。
效能優化
這個專案的另外一個目的是看一下golang原生的json api是快還是慢,有沒有提高空間。
基於流解析,無需一次讀到記憶體裡
// "encoding/json"
func Benchmark_stardard_lib(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
file, _ := os.Open("/tmp/large-file.json")
result := []struct{}{}
decoder := json.NewDecoder(file)
decoder.Decode(&result)
file.Close()
}
}
5 215547514 ns/op 71467118 B/op 272476 allocs/op
// "github.com/json-iterator/go"
func Benchmark_jsoniter(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
file, _ := os.Open("/tmp/large-file.json")
iter := jsoniter.Parse(file, 1024)
for iter.ReadArray() {
iter.Skip()
}
file.Close()
}
}
10 110209750 ns/op 4248 B/op 5 allocs/op
可以看到 json iterator 的實現對於記憶體佔用非常節省。比標準庫的實現快一倍。GC壓力就小更多了。
直接解析出 int
對於 int 的解析無需兩遍,一次性讀取。把 ParseInt 的實現合併到 json 解析的程式碼裡。
func Benchmark_jsoniter_array(b *testing.B) {
for n := 0; n < b.N; n++ {
iter := ParseString(`[1,2,3]`)
for iter.ReadArray() {
iter.ReadUint64()
}
}
}
10000000 189 ns/op
func Benchmark_json_array(b *testing.B) {
for n := 0; n < b.N; n++ {
result := []interface{}{}
json.Unmarshal([]byte(`[1,2,3]`), &result)
}
}
1000000 1327 ns/op
這個場景是 7x 的速度
無反射的,有schema的解析
按照schema解析,減少if-else判斷。直接賦值,無需經過反射
type Level1 struct {
Hello []Level2
}
type Level2 struct {
World string
}
func Benchmark_jsoniter_nested(b *testing.B) {
for n := 0; n < b.N; n++ {
iter := ParseString(`{"hello": [{"world": "value1"}, {"world": "value2"}]}`)
l1 := Level1{}
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
switch l1Field {
case "hello":
l1.Hello = readLevel1Hello(iter)
default:
iter.Skip()
}
}
}
}
func readLevel1Hello(iter *Iterator) []Level2 {
l2Array := make([]Level2, 0, 2)
for iter.ReadArray() {
l2 := Level2{}
for l2Field := iter.ReadObject(); l2Field != ""; l2Field = iter.ReadObject() {
switch l2Field {
case "world":
l2.World = iter.ReadString()
default:
iter.Skip()
}
}
l2Array = append(l2Array, l2)
}
return l2Array
}
2000000 640 ns/op
func Benchmark_json_nested(b *testing.B) {
for n := 0; n < b.N; n++ {
l1 := Level1{}
json.Unmarshal([]byte(`{"hello": [{"world": "value1"}, {"world": "value2"}]}`), &l1)
}
}
1000000 1816 ns/op
總結
golang 自帶的 json 庫其實效能很不錯了。根據benchmark(https://github.com/json-itera…)其實速度比其他的基於流的解析庫還要快(https://github.com/ugorji/go/…)。而這個庫 https://github.com/pquerna/ff… 雖然號稱更快,但是不支援流式解析(要求所有的[]byte都提前讀入到記憶體裡)。大部分情況下,就用golang自帶的就足夠好了,別瞎整一些其他的json解析庫。
如果需要pull api,或者需要額外的2x~6x效能,可以考慮:https://github.com/json-iterator/go