gjson 代表的是【get json】,sjson 代表的则是【set json】,也就是对 json 进行更新。

有的时候我们希望对一个大 json 文档中的局部进行修改,这一点利用官方 encoding/json 也是可以完成的,但还是和 gjson 类似的问题。你需要这样几步:

  1. 定义一个 struct,对应到 json 的结构;
  2. json => 结构体的反序列化,得到一个带数据的结构体;
  3. 更新你希望修改的局部数据;
  4. 结构体重新序列化为 json。

1 带来的开发成本可能是一次性的,但 2 和 4 这里的序列化成本却是每次都需要付出的,这一点很痛。
所以,tidwall 在 gjson 之外补充了 sjson 来弥补对 json 文档进行局部更新的能力。

sjson

SJSON is a Go package that provides a very fast and simple way to set a value in a json document. For quickly retrieving json values check out GJSON.

sjson 支持以简单快速的方式来设置 json 中的值。是不是感觉似曾相识,其实跟 gjson 是一样的,二者也经常配合使用。这个系列的两兄弟最大的特点就是:

  • 简单:不需要你定义结构体,有 json 文档,定义好规则就能读写;
  • 快速:性能上优势巨大,只依赖原生 Golang 库,做到了局部读写,不用对整个文档进行序列化和反序列化。

Demo

首先我们用 go get 给自己的工程添加 sjson 依赖:

go get -u github.com/tidwall/sjson

执行如下代码:

package main

import "github.com/tidwall/sjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
    value, _ := sjson.Set(json, "name.last", "Anderson")
    println(value)
}

打印出来的结果为:

{"name":{"first":"Janet","last":"Anderson"},"age":47}

发现区别了么?只是从 gjson.Get(json, path) 变成了 sjson.Set(json, path, value)而已。
我们拿到的 value 是个 string,语义上代表被更新后的 json 字符串。

Path

有了 gjson 的基础,理解 sjson 这里的 Set 逻辑就简单多了。他们共享了同一套 path 体系,你用什么方法从 gjson.Get 中查找元素,就用什么方法在 sjson.Set 中更新,区别在于 Set 中你需要提供一个更新的值罢了。
我们复习一下,假设有如下 json 文档:

{
  "name": {"first": "Tom", "last": "Anderson"},
  "age":37,
  "children": ["Sara","Alex","Jack"],
  "fav.movie": "Deer Hunter",
  "friends": [
    {"first": "James", "last": "Murphy"},
    {"first": "Roger", "last": "Craig"}
  ]
}

示例 path 对应 结果如下:

"name.last"          >> "Anderson"
"age"                >> 37
"children.1"         >> "Alex"
"friends.1.last"     >> "Craig"

有两个点需要注意:

  1. 在更新的场景下,有时候我们需要【插入】一个新的元素,这个时候可以用 index 下标 -1 来代表这个语义:
"children.-1"  >> appends a new value to the end of the children array
  1. a.num.b 这种场景下,中间的 num 数字可能有歧义,可能代表 array 或 object 中的第 num 个元素,也可能指代一个 key,这个时候可以加上 : 来指定为 key:
{
  "users":{
    "2313":{"name":"Sara"},
    "7839":{"name":"Andy"}
  }
}

"users.:2313.name"    >> "Sara"

支持的类型

sjson 支持将下面这些类型更新到 json 文档中:

  • nil
  • boolean: true, false
  • 整数
  • 浮点数
  • 字符串
  • 数组
  • map[string]interface{}

若 sjson 未识别到,将会 fallback 到 encoding/json 的 Marshaller 进行序列化。

sjson.Set(`{"key":true}`, "key", nil)
sjson.Set(`{"key":true}`, "key", false)
sjson.Set(`{"key":true}`, "key", 1)
sjson.Set(`{"key":true}`, "key", 10.5)
sjson.Set(`{"key":true}`, "key", "hello")
sjson.Set(`{"key":true}`, "key", []string{"hello", "world"})
sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"})

常见用法

初始化一个 json 文档

value, _ := sjson.Set("", "name", "Tom")
println(value)

// Output:
// {"name":"Tom"}
value, _ := sjson.Set("", "name.last", "Anderson")
println(value)

// Output:
// {"name":{"last":"Anderson"}}

新增属性

value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara")
println(value)

// Output:
// {"name":{"first":"Sara","last":"Anderson"}}

更新已经存在的属性

value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith")
println(value)

// Output:
// {"name":{"last":"Smith"}}

往 array 中新增一个元素

value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara")
println(value)

// Output:
// {"friends":["Andy","Carol","Sara"]

或者使用我们前面提到的 -1 下标

value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara")
println(value)

// Output:
// {"friends":["Andy","Carol","Sara"]

注意,-1 是自动往末尾加,如果明确下标,如上面的 friends.2,那么一定要保证 2 就是最后的下标,否则会拆入 null:

value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara")
println(value)

// Output:
// {"friends":["Andy","Carol",null,null,"Sara"]

删除属性

value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first")
println(value)

// Output:
// {"name":{"last":"Anderson"}}

删除 array 元素

指定下标

value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1")
println(value)

// Output:
// {"friends":["Andy"]}

指定最后一个

value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1")
println(value)

// Output:
// {"friends":["Andy"]}

高级用法

除了类似 gjson 中的 Bytes 函数,减少内存分配外,sjson 还支持了一些 option 来细粒度控制,我们来看一下:

// Options represents additional options for the Set and Delete functions.
type Options struct {
    // Optimistic is a hint that the value likely exists which
    // allows for the sjson to perform a fast-track search and replace.
    Optimistic bool
    // ReplaceInPlace is a hint to replace the input json rather than
    // allocate a new json byte slice. When this field is specified
    // the input json will not longer be valid and it should not be used
    // In the case when the destination slice doesn't have enough free
    // bytes to replace the data in place, a new bytes slice will be
    // created under the hood.
    // The Optimistic flag must be set to true and the input must be a
    // byte slice in order to use this field.
    ReplaceInPlace bool
}
  • Optimistic: 指明操作的值很大可能是已经存在的,这样允许 sjson 针对性地做一些优化,性能更优;
  • ReplaceInPlace: 指明更新 json 在原地完成,而不是创建出来新的 []byte 来承接 json,这样会有更好的性能收益。若开启,不能再依赖原来的 input json 内存地址,可能出现扩容。强依赖 Optimistic 为 true 才能启用。

事实上,这两个选项都是开发者声明,用以表明自己能接受多大程度的优化,通过官方benchmark我们也能看到,性能收益还是很可观的:

Benchmark_SJSON-8                       3000000           805 ns/op        1077 B/op           3 allocs/op
Benchmark_SJSON_ReplaceInPlace-8        3000000           449 ns/op           0 B/op           0 allocs/op
Benchmark_JSON_Map-8                     300000         21236 ns/op        6392 B/op         150 allocs/op
Benchmark_JSON_Struct-8                  300000         14691 ns/op        1789 B/op          24 allocs/op
Benchmark_Gabs-8                         300000         21311 ns/op        6752 B/op         150 allocs/op
Benchmark_FFJSON-8                       300000         17673 ns/op        3589 B/op          47 allocs/op
Benchmark_EasyJSON-8                    1500000          3119 ns/op        1061 B/op          13 allocs/op

开启了 ReplaceInPlace 后,耗时几乎变成了原来的一半,还是很厉害的。感兴趣的同学可以看一下 sjson-benchmark。

转自:https://github.com/tidwall/sjson-benchmarks