JSON 是一種很是流行的數據交換格式。每種編程語言都有不少操做 JSON 的庫,標準庫、第三方庫都有。Go 語言中標準庫內置了 JSON 操做庫encoding/json
。咱們以前也介紹過專門用於查詢 JSON 串的庫gjson和專門用於修改 JSON 串的庫sjson,還有一個很是方便的操做 JSON 數據的命令行工具jj。今天咱們再介紹一個 JSON 工具庫——gabs
。gabs
是一個用來查詢和修改 JSON 串的庫。它使用encoding/json
將通常的 JSON 串轉爲map[string]interface{}
,並提供便利的方法操做map[string]struct{}
。git
本文代碼使用 Go Modules。github
建立目錄並初始化:golang
$ mkdir gabs && cd gabs $ go mod init github.com/darjun/go-daily-lib/gabs
安裝gabs
,目前最新版本爲v2
,推薦使用v2
:編程
$ go get -u github.com/Jeffail/gabs/v2
使用:json
package main import ( "github.com/Jeffail/gabs/v2" "fmt" ) func main() { jObj, _ := gabs.ParseJSON([]byte(`{ "info": { "name": { "first": "lee", "last": "darjun" }, "age": 18, "hobbies": [ "game", "programming" ] } }`)) fmt.Println("first name: ", jObj.Search("info", "name", "first").Data().(string)) fmt.Println("second name: ", jObj.Path("info.name.last").Data().(string)) gObj, _ := jObj.JSONPointer("/info/age") fmt.Println("age: ", gObj.Data().(float64)) fmt.Println("one hobby: ", jObj.Path("info.hobbies.1").Data().(string)) }
首先,咱們調用gabs.ParseJSON()
方法解析傳入的 JSON 串,獲得一個gabs.Container
對象。後續經過該gabs.Container
對象來查詢和修改解析出來的數據。數組
gabs
提供 3 中查詢方式:微信
.
分隔的路徑調用Path()
方法;Search()
方法;/
分隔的路徑調用JSONPointer()
方法。上述方法內部實現最終都是調用相同的方法,只是使用上稍微有些區別。注意:app
gabs.Container
對象,咱們須要調用其Data()
獲取內部的數據,而後作一次類型轉換獲得實際的數據;Search/Path
方法返回的gabs.Container
對象內部數據爲nil
,即Data()
方法返回nil
,而JSONPointer
方法返回err
。實際使用時注意進行空指針和錯誤判斷;info.hobbies.1
;JSONPointer()
參數必須以/
開頭。運行結果:編程語言
$ go run main.go first name: lee second name: darjun age: 18 one hobby: programming
上一節中咱們介紹過,在gabs
中,路徑有 3 種表示方式。這 3 種方式對應 3 個基礎的查詢方法:工具
Search(hierarchy ...string)
:也有一個簡寫形式S
;Path(path string)
:path
以.
分隔;JSONPointer(path string)
:path
以/
分隔。它們的基本用法上面已經介紹過了,對於數組咱們還能對每一個數組元素作遞歸查詢。在下面例子中,咱們依次返回數組members
中每一個元素的name
、age
和relation
字段:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{ "user": { "name": "dj", "age": 18, "members": [ { "name": "hjw", "age": 20, "relation": "spouse" }, { "name": "lizi", "age": 3, "relation": "son" } ] } }`)) fmt.Println("member names: ", jObj.S("user", "members", "*", "name").Data()) fmt.Println("member ages: ", jObj.S("user", "members", "*", "age").Data()) fmt.Println("member relations: ", jObj.S("user", "members", "*", "relation").Data()) fmt.Println("spouse name: ", jObj.S("user", "members", "0", "name").Data().(string)) }
運行程序,輸出:
$ go run main.go member names: [hjw lizi] member ages: [20 3] member relations: [spouse son] spouse name: hjw
容易看出,在路徑中遇到數組分下面兩種狀況處理:
*
,則對全部的數組元素應用剩餘的路徑查詢,結果放在一個數組中返回;查看源碼咱們能夠知道,實際上,Path/JSONPointer
內部都是先將path
解析爲hierarchy ...string
的形式,最終都會調用searchStrict
方法:
func (g *Container) Search(hierarchy ...string) *Container { c, _ := g.searchStrict(true, hierarchy...) return c } func (g *Container) Path(path string) *Container { return g.Search(DotPathToSlice(path)...) } func (g *Container) JSONPointer(path string) (*Container, error) { hierarchy, err := JSONPointerToSlice(path) if err != nil { return nil, err } return g.searchStrict(false, hierarchy...) } func (g *Container) S(hierarchy ...string) *Container { return g.Search(hierarchy...) }
searchStrict
方法也不復雜,咱們簡單看一下:
func (g *Container) searchStrict(allowWildcard bool, hierarchy ...string) (*Container, error) { object := g.Data() for target := 0; target < len(hierarchy); target++ { pathSeg := hierarchy[target] if mmap, ok := object.(map[string]interface{}); ok { object, ok = mmap[pathSeg] if !ok { return nil, fmt.Errorf("failed to resolve path segment '%v': key '%v' was not found", target, pathSeg) } } else if marray, ok := object.([]interface{}); ok { if allowWildcard && pathSeg == "*" { tmpArray := []interface{}{} for _, val := range marray { if (target + 1) >= len(hierarchy) { tmpArray = append(tmpArray, val) } else if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil { tmpArray = append(tmpArray, res.Data()) } } if len(tmpArray) == 0 { return nil, nil } return &Container{tmpArray}, nil } index, err := strconv.Atoi(pathSeg) if err != nil { return nil, fmt.Errorf("failed to resolve path segment '%v': found array but segment value '%v' could not be parsed into array index: %v", target, pathSeg, err) } if index < 0 { return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' is invalid", target, pathSeg) } if len(marray) <= index { return nil, fmt.Errorf("failed to resolve path segment '%v': found array but index '%v' exceeded target array size of '%v'", target, pathSeg, len(marray)) } object = marray[index] } else { return nil, fmt.Errorf("failed to resolve path segment '%v': field '%v' was not found", target, pathSeg) } } return &Container{object}, nil }
實際上就是順着路徑一層層往下走,遇到數組。若是下一個部分是通配符*
,下面是處理代碼:
tmpArray := []interface{}{} for _, val := range marray { if (target + 1) >= len(hierarchy) { tmpArray = append(tmpArray, val) } else if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil { tmpArray = append(tmpArray, res.Data()) } } if len(tmpArray) == 0 { return nil, nil } return &Container{tmpArray}, nil
若是*
是路徑最後一個部分,返回全部數組元素:
if (target + 1) >= len(hierarchy) { tmpArray = append(tmpArray, val) }
不然,應用剩餘的路徑查詢每一個元素,查詢結果append
到待返回切片中:
else if res := Wrap(val).Search(hierarchy[target+1:]...); res != nil { tmpArray = append(tmpArray, res.Data()) }
另外一方面,若是不是通配符,那麼下一個路徑部分必須是索引,取這個索引的元素,繼續往下查詢:
index, err := strconv.Atoi(pathSeg)
gabs
提供了兩個方法能夠方便地遍歷數組和對象:
Children()
:返回全部數組元素的切片,若是在對象上調用該方法,Children()
將以不肯定順序返回對象全部的值的切片;ChildrenMap()
:返回對象的鍵和值。看示例:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{ "user": { "name": "dj", "age": 18, "members": [ { "name": "hjw", "age": 20, "relation": "spouse" }, { "name": "lizi", "age": 3, "relation": "son" } ] } }`)) for k, v := range jObj.S("user").ChildrenMap() { fmt.Printf("key: %v, value: %v\n", k, v) } fmt.Println() for i, v := range jObj.S("user", "members", "*").Children() { fmt.Printf("member %d: %v\n", i+1, v) } }
運行結果:
$ go run main.go key: name, value: "dj" key: age, value: 18 key: members, value: [{"age":20,"name":"hjw","relation":"spouse"},{"age":3,"name":"lizi","relation":"son"}] member 1: {"age":20,"name":"hjw","relation":"spouse"} member 2: {"age":3,"name":"lizi","relation":"son"}
這兩個方法的源碼很簡單,建議去看看~
gabs
提供了兩個方法檢查對應的路徑上是否存在數據:
Exists(hierarchy ...string)
;ExistsP(path string)
:方法名以P
結尾,表示接受以.
分隔的路徑。看示例:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{"user":{"name": "dj","age": 18}}`)) fmt.Printf("has name? %t\n", jObj.Exists("user", "name")) fmt.Printf("has age? %t\n", jObj.ExistsP("user.age")) fmt.Printf("has job? %t\n", jObj.Exists("user", "job")) }
運行:
$ go run main.go has name? true has age? true has job? false
對於類型爲數組的值,gabs
提供了幾組便捷的查詢方法。
ArrayCount/ArrayCountP
,不加後綴的方法接受可變參數做爲路徑,以P
爲後綴的方法須要傳入.
分隔的路徑;ArrayElement/ArrayElementP
。示例:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{ "user": { "name": "dj", "age": 18, "members": [ { "name": "hjw", "age": 20, "relation": "spouse" }, { "name": "lizi", "age": 3, "relation": "son" } ], "hobbies": ["game", "programming"] } }`)) cnt, _ := jObj.ArrayCount("user", "members") fmt.Println("member count:", cnt) cnt, _ = jObj.ArrayCount("user", "hobbies") fmt.Println("hobby count:", cnt) ele, _ := jObj.ArrayElement(0, "user", "members") fmt.Println("first member:", ele) ele, _ = jObj.ArrayElement(1, "user", "hobbies") fmt.Println("second hobby:", ele) }
輸出:
member count: 2 hobby count: 2 first member: {"age":20,"name":"hjw","relation":"spouse"} second hobby: "programming"
咱們可使用gabs
構造一個 JSON 串。根據要設置的值的類型,gabs
將修改的方法又分爲了兩類:原始值、數組和對象。基本操做流程是相同的:
gabs.New()
建立gabs.Container
對象,或者ParseJSON()
從現有 JSON 串中解析出gabs.Container
對象;咱們前面說過,gabs
使用三種方式來表達路徑。在設置時也能夠經過這三種方式指定在什麼位置設置值。對應方法爲:
Set(value interface{}, hierarchy ...string)
:將路徑各個部分做爲可變參數傳入便可;SetP(value interface{}, path string)
:路徑各個部分以點.
分隔;SetJSONPointer(value interface{}, path string)
:路徑各個部分以/
分隔,且必須以/
開頭。示例:
func main() { gObj := gabs.New() gObj.Set("lee", "info", "name", "first") gObj.SetP("darjun", "info.name.last") gObj.SetJSONPointer(18, "/info/age") fmt.Println(gObj.String()) }
最終生成 JSON 串:
$ go run main.go {"info":{"age":18,"name":{"first":"lee","last":"darjun"}}}
咱們也能夠調用gabs.Container
的StringIndent
方法增長前綴和縮進,讓輸出更美觀些:
fmt.Println(gObj.StringIndent("", " "))
觀察輸出變化:
$ go run main.go { "info": { "age": 18, "name": { "first": "lee", "last": "darjun" } } }
相比原始值,數組的操做複雜很多。咱們能夠建立新的數組,也能夠在原有的數組中添加、刪除元素。
func main() { gObj := gabs.New() arrObj1, _ := gObj.Array("user", "hobbies") fmt.Println(arrObj1.String()) arrObj2, _ := gObj.ArrayP("user.bugs") fmt.Println(arrObj2.String()) gObj.ArrayAppend("game", "user", "hobbies") gObj.ArrayAppend("programming", "user", "hobbies") gObj.ArrayAppendP("crash", "user.bugs") gObj.ArrayAppendP("panic", "user.bugs") fmt.Println(gObj.String()) }
咱們先經過Array/ArrayP
分別在路徑user.hobbies
和user.bugs
下建立數組,而後調用ArrayAppend/ArrayAppendP
向這兩個數組中添加元素。如今咱們應該能夠根據方法有無後綴,後綴是什麼來區分它接受什麼格式的路徑了!
運行結果:
{"user":{"bugs":["crash","panic"],"hobbies":["game","programming"]}}
實際上,咱們甚至能夠省略上面的數組建立過程,由於ArrayAppend/ArrayAppendP
若是檢測到中間路徑上沒有值,會自動建立對象。
固然咱們也能夠刪除某個索引的數組元素,使用ArrayRemove/ArrayRemoveP
方法:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{"user":{"bugs":["crash","panic"],"hobbies":["game","programming"]}}`)) jObj.ArrayRemove(0, "user", "bugs") jObj.ArrayRemoveP(1, "user.hobbies") fmt.Println(jObj.String()) }
刪除完成以後還剩下:
{"user":{"bugs":["panic"],"hobbies":["game"]}}
在指定路徑下建立對象使用Object/ObjectI/ObjectP
這組方法,其中ObjectI
是指在數組的特定索引下建立。通常地咱們使用Set
類方法就足夠了,中間路徑不存在會自動建立。
對象刪除使用Delete/DeleteP
這組方法:
func main() { jObj, _ := gabs.ParseJSON([]byte(`{"info":{"age":18,"name":{"first":"lee","last":"darjun"}}}`)) jObj.Delete("info", "name") fmt.Println(jObj.String()) jObj.Delete("info") fmt.Println(jObj.String()) }
輸出:
{"info":{"age":18}} {}
Flatten
Flatten
操做即將嵌套很深的字段提到最外層,gabs.Flatten
返回一個新的map[string]interface{}
,interface{}
爲 JSON 中葉子節點的值,鍵爲該葉子的路徑。例如:{"foo":[{"bar":"1"},{"bar":"2"}]}
執行 flatten 操做以後返回{"foo.0.bar":"1","foo.1.bar":"2"}
。
func main() { jObj, _ := gabs.ParseJSON([]byte(`{ "user": { "name": "dj", "age": 18, "members": [ { "name": "hjw", "age": 20, "relation": "spouse" }, { "name": "lizi", "age": 3, "relation": "son" } ], "hobbies": ["game", "programming"] } }`)) obj, _ := jObj.Flatten() fmt.Println(obj) }
輸出:
map[user.age:18 user.hobbies.0:game user.hobbies.1:programming user.members.0.age:20 user.members.0.name:hjw user.members.0.relation:spouse user.members.1.age:3 user.members.1.name:lizi user.members.1.relation:son user.name:dj]
咱們能夠將兩個gabs.Container
合併成一個。若是同一個路徑下有相同的鍵:
例如:
func main() { obj1, _ := gabs.ParseJSON([]byte(`{"user":{"name":"dj"}}`)) obj2, _ := gabs.ParseJSON([]byte(`{"user":{"age":18}}`)) obj1.Merge(obj2) fmt.Println(obj1) arr1, _ := gabs.ParseJSON([]byte(`{"user": {"hobbies": ["game"]}}`)) arr2, _ := gabs.ParseJSON([]byte(`{"user": {"hobbies": ["programming"]}}`)) arr1.Merge(arr2) fmt.Println(arr1) obj3, _ := gabs.ParseJSON([]byte(`{"user":{"name":"dj", "hobbies": "game"}}`)) arr3, _ := gabs.ParseJSON([]byte(`{"user": {"hobbies": ["programming"]}}`)) obj3.Merge(arr3) fmt.Println(obj3) obj4, _ := gabs.ParseJSON([]byte(`{"user":{"name":"dj", "hobbies": "game"}}`)) arr4, _ := gabs.ParseJSON([]byte(`{"user": {"hobbies": ["programming"]}}`)) arr4.Merge(obj4) fmt.Println(arr4) obj5, _ := gabs.ParseJSON([]byte(`{"user":{"name":"dj", "hobbies": {"first": "game"}}}`)) arr5, _ := gabs.ParseJSON([]byte(`{"user": {"hobbies": ["programming"]}}`)) obj5.Merge(arr5) fmt.Println(obj5) }
看結果:
{"user":{"age":18,"name":"dj"}} {"user":{"hobbies":["game","programming"]}} {"user":{"hobbies":["game","programming"],"name":"dj"}} {"user":{"hobbies":["programming","game"],"name":"dj"}} {"user":{"hobbies":[{"first":"game"},"programming"],"name":"dj"}}
gabs
是一個十分方便的操做 JSON 的庫,很是易於使用,並且代碼實現比較簡潔,值得一看。
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~