1. gjson

    之前我们介绍过gojsonq,可以方便地从一个 JSON 串中读取值。同时它也支持各种查询、汇总统计等功能。今天我们再介绍一个类似的库gjson。在上一篇文章Go 每日一库之 buntdb中我们介绍过 JSON 索引,内部实现其实就是使用gjson这个库。gjson实际上是get + json的缩写,用于读取 JSON 串,同样的还有一个sjson(set + json)库用来设置 JSON 串。

    1.1.2. 快速使用

    先安装:

    后使用:

    1. import (
    2. "fmt"
    3. "github.com/tidwall/gjson"
    4. )
    5. func main() {
    6. json := `{"name":{"first":"www.topgoer.com","last":"dj"},"age":18}`
    7. lastName := gjson.Get(json, "name.last")
    8. fmt.Println("last name:", lastName.String())
    9. age := gjson.Get(json, "age")
    10. fmt.Println("age:", age.Int())
    11. }

    使用很简单,只需要传入 JSON 串和要读取的键路径即可。注意一点细节,因为gjson.Get()函数实际上返回的是gjson.Result类型,我们要调用其相应的方法进行转换对应的类型。如上面的String()和Int()方法。

    使用很简单,只需要传入 JSON 串和要读取的键路径即可。注意一点细节,因为gjson.Get()函数实际上返回的是gjson.Result类型,我们要调用其相应的方法进行转换对应的类型。如上面的String()和Int()方法。

    1.1.3. 键路径

    键路径实际上是以.分隔的一系列键。gjson支持在键中包含通配符*和?,*匹配任意多个字符,?匹配单个字符,例如ca*可以匹配cat/cate/cake等以ca开头的键,ca?只能匹配cat/cap等以ca开头且后面只有一个字符的键。

    数组使用键名 + . + 索引(索引从 0 开始)的方式读取元素,如果键pets对应的值是一个数组,那么pets.0读取数组的第一个元素,pets.1读取第二个元素。

    数组长度使用**键名 + . + #**获取,例如pets.#返回数组pets的长度。

    如果键名中出现.,那么需要使用\进行转义。

    1. package main
    2. const json = `
    3. {
    4. "name":{"first":"Tom", "last": "Anderson"},
    5. "age": 37,
    6. "children": ["Sara", "Alex", "Jack"],
    7. "fav.movie": "Dear Hunter",
    8. "friends": [
    9. {"first": "Dale", "last":"Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
    10. {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
    11. {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
    12. ]
    13. }
    14. `
    15. func main() {
    16. fmt.Println("last name:", gjson.Get(json, "name.last"))
    17. fmt.Println("age:", gjson.Get(json, "age"))
    18. fmt.Println("children:", gjson.Get(json, "children"))
    19. fmt.Println("children count:", gjson.Get(json, "children.#"))
    20. fmt.Println("second child:", gjson.Get(json, "children.1"))
    21. fmt.Println("third child*:", gjson.Get(json, "child*.2"))
    22. fmt.Println("first c?ild:", gjson.Get(json, "c?ildren.0"))
    23. fmt.Println("fav.moive", gjson.Get(json, `fav.\moive`))
    24. fmt.Println("first name of friends:", gjson.Get(json, "friends.#.first"))
    25. fmt.Println("last name of second friend:", gjson.Get(json, "friends.1.last"))
    26. }

    前 3 个比较简单,就不赘述了。看后面几个:

    • children.#:返回数组children的长度;
    • children.1:读取数组children的第 2 个元素(注意索引从 0 开始);
    • child*.2:首先child*匹配children,.2读取第 3 个元素;
    • c?ildren.0c?ildren匹配到children,.0读取第一个元素;
    • fav.\moive:因为键名中含有.,故需要\转义;
    • friends.#.first:如果数组后#后还有内容,则以后面的路径读取数组中的每个元素,返回一个新的数组。所以该查询返回的数组所有friends的first字段组成;
    • friends.1.last:读取friends第 2 个元素的last字段。

    运行结果:

    1. last name: Anderson
    2. age: 37
    3. children: ["Sara", "Alex", "Jack"]
    4. children count: 3
    5. second child: Alex
    6. third child*: Jack
    7. first c?ild: Sara
    8. fave.moive
    9. first name of friends: ["Dale","Roger","Jane"]
    10. last name of second friend: Craig
    1. fmt.Println(gjson.Get(json, `friends.#(last="Murphy").first`))
    2. fmt.Println(gjson.Get(json, `friends.#(last="Murphy")#.first`))
    3. fmt.Println(gjson.Get(json, "friends.#(age>45)#.last"))
    4. fmt.Println(gjson.Get(json, `friends.#(first%"D*").last`))
    5. fmt.Println(gjson.Get(json, `friends.#(first!%"D*").last`))
    6. fmt.Println(gjson.Get(json, `friends.#(nets.#(=="fb"))#.first`))

    还是使用上面的 JSON 串。

    • friends.#(last="Murphy").firstfriends.#(last="Murphy")返回数组friends中第一个last为Murphy的元素,.first表示取出该元素的first字段返回;
    • friends.#(last="Murphy")#.firstfriends.#(last="Murphy")#返回数组friends中所有的last为Murphy的元素,然后读取它们的first字段放在一个数组中返回。注意与上面一个的区别;
    • friends.#(first%"D*").last:返回数组friends中第一个first字段满足模式D*的元素,取出其last字段返回;
    • friends.#(first!%"D*").last`<code>friends.#(first!%&#34;D\*&#34;)</code>返回数组friends中第一个first字段<code>\*\*</code>不<code>\*\*</code>满足模式<code>D\*</code>的元素,读取其last字段返回;
    • friends.#(nets.#(=="fb"))#.first:这是个嵌套条件,friends.#(nets.#(=="fb"))#返回数组friends的元素的nets字段中有fb的所有元素,然后取出first字段返回。

    运行结果:

    1. Dale
    2. ["Dale","Jane"]
    3. ["Craig","Murphy"]
    4. Murphy
    5. Craig
    6. ["Dale","Roger"]

    修饰符是gjson提供的非常强大的功能,和键路径搭配使用。gjson提供了一些内置的修饰符:

    • @reverse:翻转一个数组;
    • @ugly:移除 JSON 中的所有空白符;
    • @pretty:使 JSON 更易用阅读;
    • @this:返回当前的元素,可以用来返回根元素;
    • @valid:校验 JSON 的合法性;
    • @flatten:数组平坦化,即将["a", ["b", "c"]]转为["a","b","c"];
    • @join:将多个对象合并到一个对象中。

    修饰符的语法和管道类似,以|分隔键路径和分隔符。

    1. const json = `{
    2. "name":{"first":"Tom", "last": "Anderson"},
    3. "age": 37,
    4. "children": ["Sara", "Alex", "Jack"],
    5. "fav.movie": "Dear Hunter",
    6. "friends": [
    7. {"first": "Dale", "last":"Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
    8. {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
    9. {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
    10. ]
    11. }`
    12. func main() {
    13. fmt.Println(gjson.Get(json, "children|@reverse"))
    14. fmt.Println(gjson.Get(json, "children|@reverse|0"))
    15. fmt.Println(gjson.Get(json, "friends|@ugly"))
    16. fmt.Println(gjson.Get(json, "friends|@pretty"))
    17. fmt.Println(gjson.Get(json, "@this"))
    18. nestedJSON := `{"nested": ["one", "two", ["three", "four"]]}`
    19. fmt.Println(gjson.Get(nestedJSON, "nested|@flatten"))
    20. userJSON := `{"info":[{"name":"dj", "age":18},{"phone":"123456789","email":"dj@example.com"}]}`
    21. fmt.Println(gjson.Get(userJSON, "info|@join"))
    22. }

    children|@reverse先读取数组children,然后使用修饰符@reverse翻转之后返回,输出:

    1. ["Jack","Alex","Sara"]

    children|@reverse|0在上面翻转的基础上读取第一个元素,即原数组的最后一个元素,输出:

    friends|@ugly移除friends数组中的所有空白字符,返回一行长长的字符串:

    1. [{"first":"Dale","last":"Murphy","age":44,"nets":["ig","fb","tw"]},{"first":"Roger","last":"Craig","age":68,"nets":["fb","tw"]},{"first":"Jane","last":"Murphy","age":47,"nets":["ig","tw"]}]

    friends|@pretty格式化friends数组,使之更易读:

    1. [
    2. {
    3. "first": "Dale",
    4. "last": "Murphy",
    5. "age": 44,
    6. "nets": ["ig", "fb", "tw"]
    7. },
    8. {
    9. "first": "Roger",
    10. "last": "Craig",
    11. "age": 68,
    12. "nets": ["fb", "tw"]
    13. },
    14. {
    15. "first": "Jane",
    16. "last": "Murphy",
    17. "age": 47,
    18. "nets": ["ig", "tw"]
    19. }
    20. ]

    @this返回原始的 JSON 串。

    @flatten将数组nested的内层数组平坦到外层后返回,即将所有内层数组的元素依次添加到外层数组后面并移除内层数组,输出:

    1. ["one","two","three", "four"]

    @join将一个数组中的各个对象合并到一个中,例子中将数组中存放的部分个人信息合并成一个对象返回:

    1. {"name":"dj","age":18,"phone":"123456789","email":"dj@example.com"}

    1.1.5. 修饰符参数

    1. fmt.Println(gjson.Get(json, `friends|@pretty:{"sortKeys":true}`))

    最终按键名顺序输出 JSON 串:

    1. [
    2. {
    3. "age": 44,
    4. "first": "Dale",
    5. "last": "Murphy",
    6. },
    7. {
    8. "age": 68,
    9. "first": "Roger",
    10. "last": "Craig",
    11. "nets": ["fb", "tw"]
    12. },
    13. "age": 47,
    14. "first": "Jane",
    15. "last": "Murphy",
    16. "nets": ["ig", "tw"]
    17. }
    18. ]

    当然还可以指定每行缩进indent(默认两个空格),每行开头字符串prefix(默认为空串)和一行最多显示字符数width(默认 80 字符)。下面在每行前增加两个空格:

    1. fmt.Println(gjson.Get(json, `friends|@pretty:{"sortKeys":true,"prefix":" "}`))

    1.1.6. 自定义修饰符

    如此强大的功当然要支持自定义!gjson使用AddModifier()添加一个修饰符,传入一个名字和类型为func(json arg string) string的处理函数。处理函数接受待处理的 JSON 值和修饰符参数,返回处理后的结果。下面编写一个转换大小写的修饰符:

    输出:

    1. ["SARA", "ALEX", "JACK"]
    2. ["sara", "alex", "jack"]

    gjson提供..语法可以将多行数据看成一个数组,每行数据是一个元素:

    1. const json = `
    2. {"name": "Gilbert", "age": 61}
    3. {"name": "Alexa", "age": 34}
    4. {"name": "May", "age": 57}
    5. {"name": "Deloise", "age": 44}`
    6. func main() {
    7. fmt.Println(gjson.Get(json, "..#"))
    8. fmt.Println(gjson.Get(json, "..1"))
    9. fmt.Println(gjson.Get(json, "..#.name"))
    10. fmt.Println(gjson.Get(json, `..#(name="May").age`))
    11. }
    • ..#:返回有多少行 JSON 数据;
    • ..1:返回第一行,即{"name": "Gilbert", "age": 61};
    • ..#.name:#后再接路径,表示对数组中每个元素读取后面的路径,将读取到的值组成一个新数组返回;..#.name表示读取每一行中的name字段,最终返回["Gilbert","Alexa","May","Deloise"];
    • ..#(name="May").age:括号中的内容(name="May")表示条件,所以该条含义为取name为"May"的行中的age字段。

    gjson还提供了遍历 JSON 行的方法:gjson.ForEachLine(),参数为 JSON 串和类型为func(line gjson.Result) bool的回调函数。回调返回false时遍历停止。下面代码读取输出每一行的name字段:

    1. gjson.ForEachLine(json, func(line gjson.Result) bool {
    2. fmt.Println("name:", gjson.Get(line.String(), "name"))
    3. return true
    4. })

    1.1.8. 遍历

    上面我们介绍了遍历 JSON 行的方式,实际上gjson还提供了通用的遍历数组和对象的方式。gjson.Get()方法返回一个gjson.Result类型的对象,json.Result提供了ForEach()方法用于遍历。该方法接受一个类型为func (key, value gjson.Result) bool的回调函数。遍历对象时key和value分别为对象的键和值;遍历数组时,value为数组元素,key为空(不是索引)。回调返回false时,遍历停止。

    1. const json = `
    2. {
    3. "name":"dj",
    4. "age":18,
    5. "pets": ["cat", "dog"],
    6. "contact": {
    7. "phone": "123456789",
    8. "email": "dj@example.com"
    9. }
    10. }`
    11. func main() {
    12. pets := gjson.Get(json, "pets")
    13. pets.ForEach(func(_, pet gjson.Result) bool {
    14. fmt.Println(pet)
    15. return true
    16. })
    17. contact := gjson.Get(json, "contact")
    18. contact.ForEach(func(key, value gjson.Result) bool {
    19. fmt.Println(key, value)
    20. return true
    21. })
    22. }

    1.1.9. 校验 JSON

    调用gjson.Get()时,gjson假设我们传入的 JSON 串是合法的。如果 JSON 非法也不会panic,这时会返回不确定的结果:

    1. func main() {
    2. const json = `{"name":dj,age:18}`
    3. fmt.Println(gjson.Get(json, "name"))
    4. }

    上面 JSON 串是非法的,dj和age都没有加上双引号(实际上习惯了 Go 语言map的写法,很容易把 JSON 写成这样😭)。上面代码输出18,显然是错误的。我们可以使用gjson.Valid()检测 JSON 串是否合法:

    1. if !gjson.Valid(json) {
    2. fmt.Println("error")
    3. } else {
    4. fmt.Println("ok")
    5. }

    调用gjson.Get()一次只能读取一个值,多次调用又比较麻烦,gjson提供了GetMany()可以一次读取多个值,返回一个数组[]gjson.Result。

    1. const json = `
    2. {
    3. "name":"dj",
    4. "age":18,
    5. "pets": ["cat", "dog"],
    6. "contact": {
    7. "phone": "123456789",
    8. "email": "dj@example.com"
    9. }
    10. }`
    11. func main() {
    12. results := gjson.GetMany(json, "name", "age", "pets.#", "contact.phone")
    13. for _, result := range results {
    14. fmt.Println(result)
    15. }

    上面代码返回字段name、age、数组pets的长度和contact.phone字段。