持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天, 点击查看活动详情
json 的反序列化是线上服务中非常耗 cpu 的操作,很多时候我们只需要读取 json 中的某个属性值。这个时候对整个 json 进行反序列化显然成本过高,有没有什么办法能简化操作,不需要预先定义一个结构体就能解析 json 呢?
gjson 就是对这个问题的答案。这篇文章我们来了解一下它能做什么,性能如何。
gjson
GJSON is a Go package that provides a fast and simple way to get values from a json document. It has features such as one line retrieval , dot notation paths , iteration , and parsing json lines .
gjson 是 tidwall 下的经典开源库,和 sjson 常常一起出现。前者代表【get json】,后者代表【set json】,分别侧重于读写 json 结构。
其实从 json 结构中拿到某个属性值,并不是一件很难的事。定义一个结构体,
json.Unmarshal
就能搞定。那为什么我们需要 gjson ?
原因有两点:
json.Unmarshal
的成本来读取某个值。
首先我们通过 go get 添加 gjson 依赖:
$ go get -u github.com/tidwall/gjson
基础场景下,我们只需要记住 gjson.Get()
这个函数即可,使用 .
来分隔不同的层级:
package main
import "github.com/tidwall/gjson"
const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
func main() {
value := gjson.Get(json, "name.last")
println(value.String())
上面这个case中,我们传入了一个 json 字符串,第一级属性为 name 和 age,在 name 下又包含了 first 和 last 两个属性。用 name.last
代表的语义就是,拿到 name 下的属性 last 的值。最终打印的结果为:
Prichard
这样的用法非常简单,一个简单的 Get 调用即可。
json 类型
一个 json 结构,其实能包含的类型无非几种,gjson 也都定义了出来:
// Type is Result type
type Type int
const (
// Null is a null json value
Null Type = iota
// False is a json false boolean
False
// Number is json number
Number
// String is a json string
String
// True is a json true boolean
// JSON is a raw block of JSON
null:底层用 nil 来承接;
布尔类型:true 和 false;
数字类型:整型和浮点型都归于此,底层是个 float64,需要的时候转为整型;
字符串类型:底层是 string;
json 类型(指代子类型,嵌套 json 对象)。
Valid
有些时候我们只需要校验某个字符串是否为合法的 json 文档,这时候直接用 Valid 函数即可:
// Valid returns true if the input is valid json.
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
func Valid(json string) bool {
_, ok := validpayload(stringBytes(json), 0)
return ok
下面我们来走近 Get 函数,看看它提供了什么能力。
(好的开源库一定是通过【代码注释】+ 【单测】就能讲清楚怎么用,而不是靠冗长的说明,这一点 gjson 做的也很好)
func Get(json, path string) Result
和上面用法一样,函数签名非常简单:
第一个参数:一个json字符串;
第二个参数:表达要查询的值的路径;
返回值:gjson 封装的 Result 结构,包含了解析结果的上下文。
响应 Result
这里我们附上 Result 的定义以及相关方法签名:
// Result represents a json value that is returned from Get().
type Result struct {
// Type is the json type
Type Type
// Raw is the raw json
Raw string
// Str is the json string
Str string
// Num is the json number
Num float64
// Index of raw value in original json, zero means index unknown
Index int
// Indexes of all the elements that match on a path containing the '#'
// query character.
Indexes []int
// String returns a string representation of the value.
func (t Result) String() string
// Bool returns an boolean representation.
func (t Result) Bool() bool
// Int returns an integer representation.
func (t Result) Int() int64
// Uint returns an unsigned integer representation.
func (t Result) Uint() uint64
// Float returns an float64 representation.
func (t Result) Float() float64
// Time returns a time.Time representation.
func (t Result) Time() time.Time
// Array returns back an array of values.
// If the result represents a null value or is non-existent, then an empty
// array will be returned.
// If the result is not a JSON array, the return value will be an
// array containing one result.
func (t Result) Array() []Result
// IsObject returns true if the result value is a JSON object.
func (t Result) IsObject() bool
// IsArray returns true if the result value is a JSON array.
func (t Result) IsArray() bool
// IsBool returns true if the result value is a JSON boolean.
func (t Result) IsBool() bool
// ForEach iterates through values.
// If the result represents a non-existent value, then no values will be
// iterated. If the result is an Object, the iterator will pass the key and
// value of each item. If the result is an Array, the iterator will only pass
// the value of each item. If the result is not a JSON array or object, the
// iterator will pass back one value equal to the result.
func (t Result) ForEach(iterator func(key, value Result) bool)
// Map returns back a map of values. The result should be a JSON object.
// If the result is not a JSON object, the return value will be an empty map.
func (t Result) Map() map[string]Result
// Value returns one of these types:
// bool, for JSON booleans
// float64, for JSON numbers
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
// map[string]interface{}, for JSON objects
// []interface{}, for JSON arrays
func (t Result) Value() interface{}
Result 记录了该 json 对象的类型,保留了原始的 json,以及字符串,数字的表示,加上下标。
result.Type // can be String, Number, True, False, Null, or JSON
result.Str // holds the string
result.Num // holds the float64 number
result.Raw // holds the raw json
result.Index // index of raw value in original json, zero means index unknown
result.Indexes // indexes of all the elements that match on a path containing the '#' query character.
这些属性其实从调用方的角度,不用过于关心,重点看方法。
除了支持转换为【字符串】,【布尔类型】,【浮点数】,【整数】这些基础类型外,Result 还支持转换为【数组】,【Map】。
校验属性是否存在
这一点是通过 Exists()
方法实现的,示例:
value := gjson.Get(json, "name.last")
if !value.Exists() {
println("no last name")
} else {
println(value.String())
// Or as one step
if gjson.Get(json, "name.last").Exists() {
println("has a last name")
Result 扩展出来的迭代器:ForEach,支持对 Array 和 json 对象进行遍历。key 和 value 会作为参数传入 iterator 函数,如果我们返回了 false,就会终止迭代。写法示例:
result := gjson.Get(json, "programmers")
result.ForEach(func(key, value gjson.Result) bool {
println(value.String())
return true // keep iterating
Value
注意最后 Result 还提供了一个 Value()
方法,我们可以从此获取一个 interface{},后续需要自行断言,类型映射如下:
boolean >> bool
number >> float64
string >> string
null >> nil
array >> []interface{}
object >> map[string]interface{}
入参 json
注意,Get 函数的第一个入参需要我们传入一个 json 字符串,很多人会不注意这一点,如果我们传入的 json 字符串是不合法的,业界通常有两种处理方式:
直接 panic
处理结果 undefined
第一种方案其实很符合我们经常在软件开发中强调的【fail fast 原则】,不要自作聪明地加一些不必要的兜底,该报错,就报错,否则你的 caller 会 abuse 这种兜底行为,你就无法再下掉了。
这也是为什么 Golang 官方库中尤其是 reflect 包下你会频繁见到在类型对不上的情况下直接 panic,而不是很别扭地去适配。
第二种方案也很常见,所以大家一定要看好文档和签名注释。否则你传入了一个不合法的 input,还指望拿到【正确】的 output,一定会影响到业务后续逻辑。
gjson.Get
函数的处理如下:
This function expects that the json is well-formed, and does not validate.
Invalid json will not panic, but it may return back unexpected results.
If you are consuming JSON from an unpredictable source then you may want to use the Valid function first.
总结一下:Get 函数期望你传入的 json 参数是一个合法的 json 字符串,如果不是,函数调用也并不会 panic,而是返回预期外的结果。如果你的参数,有可能出现不合法的情况(比如来自外部输入,可能出错),那么建议先调用 gjson.Valid
函数,校验是否合法,再调用 gjson.Get
。
入参 path
下面我们来好好看一看,这个 path 参数怎么传。除了上面示例里提到的 .
还有什么运算符可以用。
// A path is a series of keys separated by a dot.
// A key may contain special wildcard characters '*' and '?'.
// To access an array value use the index as the key.
// To get the number of elements in an array or to access a child path, use
// the '#' character.
// The dot and wildcard character can be escaped with '\'.
参照源码 Get 函数的注释,我们可以得出以下结论:
path 中包含一系列属性 key,用 .
来连接,也就是需要遵循 a.b.c
这样的格式;
属性 key 除了精准匹配外,还支持 *
和 ?
这样的通配符;
支持通过 index 下标作为属性 key;
支持 #
作为 len() 这样获取 array 中的元素个数,或获取子 path;
可以用 \
来 escape .
符号。
这里我们看一下官方给出的示例:
"name": {"first": "Tom", "last": "Anderson"},
"age":37,
"children": ["Sara","Alex","Jack"],
"friends": [
{"first": "James", "last": "Murphy"},
{"first": "Roger", "last": "Craig"}
path 和对应结果:
"name.last" >> "Anderson"
"age" >> 37
"children" >> ["Sara","Alex","Jack"]
"children.#" >> 3
"children.1" >> "Alex"
"child*.2" >> "Jack"
"c?ildren.0" >> "Sara"
"friends.#.first" >> ["James","Roger"]
这里需要注意,最后一个示例中,我们需要获取 friends 下所有 first 的值,转为一个 array 返回。
直观上会期待这里是"friends.*.first"
,毕竟 *
语义上就指代所有。但事实上这里为了跟 key 中的 *
区分开,使用的是 #
来连接。这也对应的 #
的第二种用法。
此外,如果我们希望对 child path 中每个元素都加上条件(类比我们 SQL 中的 Where),可以使用 #()
的语法来实现,看下写法:
friends.#(last=="Murphy").first >> "Dale"
friends.#(last=="Murphy")#.first >> ["Dale","Jane"]
friends.#(age>45)#.last >> ["Craig","Murphy"]
friends.#(first%"D*").last >> "Murphy"
friends.#(first!%"D*").last >> "Craig"
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
GetBytes
这里很有意思,上面可以看到,其实 path 就是 gjson 核心的查询逻辑,类似于 SQL 之于关系型数据库。当我们希望从一个 json 结构中查询到符合条件的某些 key 的值,只需要想清楚我们的 path 长什么样就好。
所以,Get
函数的入参看起来是绰绰有余的。
那为什么还有个 GetBytes
呢?
这就又回到了 Golang 中的经典话题:怎样实现 string 和 []byte 的高效转换。这部分我们随后的文章会好好分析一下。目前,大家只需要记住:GetBytes
函数功能上和 Get
函数是一致的,区别在于 input 的 json 字符串,从 string 变成了 []byte。
如果你的 json 输入源是个 []byte,那么更加建议直接用 GetBytes
函数来获取 Result,而不是手动通过string()
函数转换,这样性能更好。
// GetBytes searches json for the specified path.
// If working with bytes, this method preferred over Get(string(data), path)
func GetBytes(json []byte, path string) Result {
return getBytes(json, path)
// getBytes casts the input json bytes to a string and safely returns the
// results as uniquely allocated data. This operation is intended to minimize
// copies and allocations for the large json string->[]byte.
func getBytes(json []byte, path string) Result
这里的 getBytes 的实现用到了 unsafe 下的一些能力,实现了高效的转换。底层还是会先将 []byte 转换为 string:
result = Get(*(*string)(unsafe.Pointer(&json)), path)
感兴趣的同学可以看下源码,我们这里不做赘述,大家感受一下就好。随后的文章中我们会回过头来分析下getBytes
的实现里怎样做性能优化,同时也保证安全性的。
GetMany / GetManyBytes
其实就是 Get 函数的一个 Multi 的版本,支持传入多个 path,最终获取一个 []Result
。底层实现可以看到只是简单封装了一下 Get 函数:
// GetMany searches json for the multiple paths.
// The return value is a Result array where the number of items
// will be equal to the number of input paths.
func GetMany(json string, path ...string) []Result {
res := make([]Result, len(path))
for i, path := range path {
res[i] = Get(json, path)
return res
其实还有一个 GetManyBytes
函数,也是类似的:
// GetManyBytes searches json for the multiple paths.
// The return value is a Result array where the number of items
// will be equal to the number of input paths.
func GetManyBytes(json []byte, path ...string) []Result {
res := make([]Result, len(path))
for i, path := range path {
res[i] = GetBytes(json, path)
return res
A modifier is a path component that performs custom processing on the json.
从 1.2 版本开始,gjson 的能力得到了进一步升级,其实还是围绕着 Query Language 这个领域不断丰富自己的能力。此前我们只能通过 .
分隔,配合 #
, *
, ?
通配符来查询。
有了修饰符之后,我们可以实现对 json 查询进行更多个性化处理。完整的修饰符列表大家可以参照官方文档,这里我们列出几个常用的:
@reverse
: 反转一个 array 或者对象中的元素;
@ugly
: 去除所有 json 文档中的空格;
@pretty
: 让 json 文档可读性更高;
@this
: 返回当前元素,可以用来查询根节点;
@valid
: 确保 json 文档是合法的;
@flatten
: 平铺 array;
@join
: 将多个对象连接为一个对象,类似 SQL 中的 join;
@keys
: 返回一个对象中的所有 key;
@values
: 返回一个对象中的所有 value;
@tostr
: 将 json 转为一个字符串;
@fromstr
: 将一个字符串转换为一个 json;
@group
: 将对象 array 分组. 参照 e4fc67c.
以 reverse 为例,我们看看用法:
"name": {"first": "Tom", "last": "Anderson"},
"children": ["Sara","Alex","Jack"]
"children|@reverse" >> ["Jack","Alex","Sara"]
"children|@reverse|0" >> "Jack"
修饰符可以有一些自己的个性化配置,接收参数,类似函数传参的形式,比如 pretty 修饰符下,我们可以指定排序的key:
@pretty:{"sortKeys":true}
这样查询回来的 json 文档,就会按照 key 来排序,注意下面,从 a 开始到 n 字母序:
"age":37,
"children": ["Sara","Alex","Jack"],
"fav.movie": "Deer Hunter",
"friends": [
{"age": 44, "first": "Dale", "last": "Murphy"},
{"age": 68, "first": "Roger", "last": "Craig"},
{"age": 47, "first": "Jane", "last": "Murphy"}
"name": {"first": "Tom", "last": "Anderson"}
JSON Line
有的时候我们的输入不是一个 json 文档,而是多个,外层并没有一个统一的 key 来包装。
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}
如上所示,这种情况下,gjson 也提供了相关的能力进行处理:
..# >> 4
..1 >> {"name": "Alexa", "age": 34}
..3 >> {"name": "Deloise", "age": 44}
..#.name >> ["Gilbert","Alexa","May","Deloise"]
..#(name="May").age >> 57
发现了么?就是一个 ..
的前缀,它会自动把输入的多行 json,当做一个 array 来处理。
我们可以用 gjson.ForEachLine
来处理:
gjson.ForEachLine(json, func(line gjson.Result) bool{
println(line.String())
return true
除了用 .
这样的写法,gjson 还支持链式写法,我们可以简易 Parse 一下,然后通过多个 Get 拿到内层的值,注意 Parse 不需要我们直接填 path 参数了,只要提供一个正常合法的 json 字符串输入即可,我们可以基于拿到的 Result 后续 Get 所需的值。
// Parse parses the json and returns a result.
// This function expects that the json is well-formed, and does not validate.
// Invalid json will not panic, but it may return back unexpected results.
// If you are consuming JSON from an unpredictable source then you may want to
// use the Valid function first.
func Parse(json string) Result
// Get searches result for the specified path.
// The result should be a JSON array or object.
func (t Result) Get(path string) Result {
r := Get(t.Raw, path)
if r.Indexes != nil {
for i := 0; i < len(r.Indexes); i++ {
r.Indexes[i] += t.Index
} else {
r.Index += t.Index
return r
所以,下面三种写法是等价的:
gjson.Parse(json).Get("name").Get("last")
gjson.Get(json, "name").Get("last")
gjson.Get(json, "name.last")
json 转 map
json 和 map[string]interface{} 之间的转换是常见的诉求,基于 gjson,我们可以复用上面提到的 Parse
函数以及 Result 的 Value
方法来实现这一点:
m, ok := gjson.Parse(json).Value().(map[string]interface{})
if !ok {
// not a map
gjson 不用完成反序列化,相较于 encoding/json 等方案有着天然的优势。官方提供了一个专门的 gjson-benchmark 来比对性能差异,感兴趣的同学可以看一下,这里 benchmark 的写法值得学习。
这里我们贴一下官方的结论:
BenchmarkGJSONGet-16 11644512 311 ns/op 0 B/op 0 allocs/op
BenchmarkGJSONUnmarshalMap-16 1122678 3094 ns/op 1920 B/op 26 allocs/op
BenchmarkJSONUnmarshalMap-16 516681 6810 ns/op 2944 B/op 69 allocs/op
BenchmarkJSONUnmarshalStruct-16 697053 5400 ns/op 928 B/op 13 allocs/op
BenchmarkJSONDecoder-16 330450 10217 ns/op 3845 B/op 160 allocs/op
BenchmarkFFJSONLexer-16 1424979 2585 ns/op 880 B/op 8 allocs/op
BenchmarkEasyJSONLexer-16 3000000 729 ns/op 501 B/op 5 allocs/op
BenchmarkJSONParserGet-16 3000000 366 ns/op 21 B/op 0 allocs/op
BenchmarkJSONIterator-16 3000000 869 ns/op 693 B/op 14 allocs/op
可以看到,gjson 来获取属性值,和原生的 unmarshal,decode 比有着量级上的优势。其实不同的 json 文档,查询不同的 key,相信也会有一些明显差异。建议大家自己也写一下 benchmark 体验一番。
jsoniter 因为也提供了类似的 ReadObject API,相对而言性能上基本到了一个量级,但还是会弱一些。
gjson 在只使用标准库的情况下,针对不同 json 类型进行适配,提供了高性能的查询能力,这一点是很了不起的。同时,基于内置以及自定义的修饰符能力,我们还可以定义更多查询组合。