Go 每日一庫之 gabs

簡介

JSON 是一種很是流行的數據交換格式。每種編程語言都有不少操做 JSON 的庫,標準庫、第三方庫都有。Go 語言中標準庫內置了 JSON 操做庫encoding/json。咱們以前也介紹過專門用於查詢 JSON 串的庫gjson和專門用於修改 JSON 串的庫sjson,還有一個很是方便的操做 JSON 數據的命令行工具jj。今天咱們再介紹一個 JSON 工具庫——gabsgabs是一個用來查詢和修改 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

  • 3 個方法最終都返回一個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

查詢 JSON 串

上一節中咱們介紹過,在gabs中,路徑有 3 種表示方式。這 3 種方式對應 3 個基礎的查詢方法:工具

  • Search(hierarchy ...string):也有一個簡寫形式S
  • Path(path string)path.分隔;
  • JSONPointer(path string)path/分隔。

它們的基本用法上面已經介紹過了,對於數組咱們還能對每一個數組元素作遞歸查詢。在下面例子中,咱們依次返回數組members中每一個元素的nameagerelation字段:

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對象;
  • 調用方法設置或修改鍵值,也能夠刪除一些鍵;
  • 生成最終的 JSON 串。

原始值

咱們前面說過,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.ContainerStringIndent方法增長前綴和縮進,讓輸出更美觀些:

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.hobbiesuser.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😄

參考

  1. gabs GitHub:https://github.com/Jeffail/gabs
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索