使用空接口在 Go 中正确 json 解组

Proper json unmarshaling in Go with the empty interface

我目前正在学习 golang 并且(可能和我之前的许多人一样)我正在尝试正确理解空接口。

作为练习,我正在阅读 Postman 生成的一个大 json 文件,并尝试仅访问一个字段(在许多可用字段中)。

这是 json 的简单表示,没有我不想阅读的不必要字段(但它们仍然存在):

{
    "results": [
        {
            "times": [
                1,
                2,
                3,
                4
                ]
        }
    ]
}

由于 json 对象很大,我选择不使用自定义结构对其进行解组,而是决定使用空接口 interface{}

一段时间后,我设法获得了一些工作代码,但我很确定这不是正确的方法。

byteValue, _ := ioutil.ReadAll(jsonFile)

var result map[string]interface{}
err = json.Unmarshal(byteValue, &result)
if err != nil {
    log.Fatalln(err)
}

// ESPECIALLY UGLY
r := result["results"].([]interface{})
r1 := r[0].(map[string]interface{})
r2 := r1["times"].([]interface{})

times := make([]float64, len(r2))
for i := range r2 {
    times[i] = r2[i].(float64)
}

是否有更好的方法来浏览我的 json 对象,而无需在每次深入对象时都实例化新变量?

  1. 即使JSON很大,你只需要定义你真正关心的字段
  2. 如果密钥无效,您只需要使用 JSON 标签 Go 标识符(在这种情况下键是标识),即便如此,有时您也可以通过使用 map[string]something
  3. 来避免它
  4. 除非您需要子 struct 用于某些功能或诸如此类的东西,否则您不需要定义它们
  5. 除非你需要重用类型,你甚至不必定义它,你可以在声明时定义结构

示例:

package main

import (
   "encoding/json"
   "fmt"
)

const s = `
{
   "results": [
      {
         "times": [1, 2, 3, 4]
      }
   ]
}
`

func main() {
   var t struct {
      Results []struct {
         Times []int
      }
   }
   json.Unmarshal([]byte(s), &t)
   fmt.Printf("%+v\n", t) // {Results:[{Times:[1 2 3 4]}]}
}

[...] trying to access just one field (out of the many available).

对于这个具体的用例,我会使用一个库来查询和访问已知路径中的单个值,例如:

https://github.com/jmespath/go-jmespath

另一方面,如果您正在练习如何访问 JSON 中的嵌套值,我建议您尝试编写一个递归函数,该函数遵循未知结构中的路径与 go-jmespath 一样(但简单)。

好吧,我挑战自己,花了一个小时写这篇文章。有用。不确定性能或错误,而且它真的很有限:)

https://play.golang.org/p/dlIsmG6Lk-p

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "strings"
)

func main() {

    // I Just added a bit more of data to the structure to be able to test different paths
    fileContent := []byte(`
    {"results": [
            {"times": [
                    1,
                    2,
                    3,
                    4
        ]},
        {"times2": [
                    5,
                    6,
                    7,
                    8
        ]},
        {"username": "rosadabril"},
        {"age": 42},
        {"location": [41.5933262, 1.8376757]}
        
    ],
    "more_results": {
        "nested_1": {
            "nested_2":{
                "foo": "bar"
            }
        }
    }
    }`)

    var content map[string]interface{}
    if err := json.Unmarshal(fileContent, &content); err != nil {
        panic(err)
    }

    // some paths to test
    valuePaths := []string{
        "results.times",
        "results.times2",
        "results.username",
        "results.age",
        "results.doesnotexist",
        "more_results.nested_1.nested_2.foo",
    }

    for _, p := range valuePaths {
        breadcrumbs := strings.Split(p, ".")

        value, err := search(breadcrumbs, content)
        if err != nil {
            fmt.Printf("\nerror searching '%s': %s\n", p, err)
            continue
        }

        fmt.Printf("\nFOUND A VALUE IN: %s\n", p)
        fmt.Printf("Type: %T\nValue: %#v\n", value, value)
    }

}

// search is our fantastic recursive function! The idea is to search in the structure in a very basic way, for complex querying use jmespath
func search(breadcrumbs []string, content map[string]interface{}) (interface{}, error) {
    // we should never hit this point, but better safe than sorry and we could incurr in an out of range error (check line 82)
    if len(breadcrumbs) == 0 {
        return nil, errors.New("ran out of breadcrumbs :'(")
    }

    // flag that indicates if we are at the end of our trip and whe should return the value without more checks
    lastBreadcrumb := len(breadcrumbs) == 1

    // current breadcrumb is always the first element.
    currentBreadcrumb := breadcrumbs[0]

    if value, found := content[currentBreadcrumb]; found {
        if lastBreadcrumb {
            return value, nil
        }
        // if the value is a map[string]interface{}, go down the rabbit hole, recursion!
        if aMap, isAMap := value.(map[string]interface{}); isAMap {
            // we are calling ourselves popping the first breadcrumb and passing the current map
            return search(breadcrumbs[1:], aMap)
        }
        // if it's an array of interfaces the thing gets complicated :(
        if anArray, isArray := value.([]interface{}); isArray {
            for _, something := range anArray {
                if aMap, isAMap := something.(map[string]interface{}); isAMap && len(breadcrumbs) > 1 {
                    if v, err := search(breadcrumbs[1:], aMap); err == nil {
                        return v, nil
                    }
                }
            }
        }
    }
    return nil, errors.New("woops, nothing here")
}