跳转到主要内容

在这篇文章中,我们将学习如何以最简单的方式在 Go 中使用 JSON。

我们将学习如何将 JSON 原始数据(字符串或字节)转换为 Go 类型,如结构、数组和切片,以及非结构化数据,如映射和空接口。

banner

JSON 被用作数据序列化的事实标准,在本文结束时,您将熟悉如何在 Go Unmarshaling Raw JSON Data 中对 JSON 进行编组(编码)和解组(解码)

Unmarshaling Raw JSON Data


Go 的 JSON 标准库提供的 Unmarshal 函数可以让我们以 []byte 变量的形式解析原始 JSON 数据。

我们可以将 JSON 字符串转换为字节,并将数据解组为变量地址:

import "encoding/json"
//...

// ... 
myJsonString := `{"some":"json"}`

// `&myStoredVariable` is the address of the variable we want to store our
// parsed data in
json.Unmarshal([]byte(myJsonString), &myStoredVariable)
//...


让我们看看 myStoredVariable 的不同变量类型,以及何时应该使用它们。

使用 JSON 时会遇到两种类型的数据:

  • 结构化数据
  • 非结构化数据


结构化数据(将 JSON 解码为结构)


“结构化数据”是指您事先知道格式的数据。例如,假设您有一个鸟类对象,其中每只鸟都有一个种类字段和一个描述字段:

{
  "species": "pigeon",
  "description": "likes to perch on rocks"
}


要使用此类数据,请创建一个反映您要解析的数据的结构。在我们的例子中,我们将创建一个具有 Species 和 Description 属性的鸟类结构:

type Bird struct {
  Species string
  Description string
}

并将其解组如下:

birdJson := `{"species": "pigeon","description": "likes to perch on rocks"}`
var bird Bird    
json.Unmarshal([]byte(birdJson), &bird)
fmt.Printf("Species: %s, Description: %s", bird.Species, bird.Description)
//Species: pigeon, Description: likes to perch on rocks


 

按照惯例,Go 使用与不区分大小写的 JSON 属性中相同的标题大小写属性名称。因此,我们 Bird 结构中的 Species 属性将映射到该物种,或 Species 或 sPeCiEs JSON 属性。

JSON 数组


让我们看看如何解码对象数组,如下所示:

[
  {
    "species": "pigeon",
    "decription": "likes to perch on rocks"
  },
  {
    "species":"eagle",
    "description":"bird of prey"
  }
]


由于数组的每个元素都具有 Bird 结构的结构,因此您可以通过创建一个小鸟切片来解组它:

birdJson := `[{"species":"pigeon","decription":"likes to perch on rocks"},{"species":"eagle","description":"bird of prey"}]`
var birds []Bird
json.Unmarshal([]byte(birdJson), &birds)
fmt.Printf("Birds : %+v", birds)
//Birds : [{Species:pigeon Description:} {Species:eagle Description:bird of prey}]


 

嵌套对象


现在,考虑当您有一个名为 Dimensions 的属性时的情况,该属性测量相关鸟的高度和长度:

{
  "species": "pigeon",
  "decription": "likes to perch on rocks"
  "dimensions": {
    "height": 24,
    "width": 10
  }
}


与我们之前的示例一样,我们需要在 Go 代码中镜像对象的结构。要添加嵌套维度对象,让我们创建一个维度结构:

type Dimensions struct {
  Height int
  Width int
}


现在,Bird 结构将包含一个Dimensions 字段:

type Bird struct {
  Species string
  Description string
  Dimensions Dimensions
}


我们可以使用与以前相同的方法来解组这些数据:

birdJson := `{"species":"pigeon","description":"likes to perch on rocks", "dimensions":{"height":24,"width":10}}`
var birds Bird
json.Unmarshal([]byte(birdJson), &birds)
fmt.Printf(bird)
// {pigeon likes to perch on rocks {24 10}}

 

原始类型


在使用 JSON 时,我们主要处理复杂的对象或数组,但像 3、3.1412 和“birds”这样的数据也是有效的 JSON 字符串。

我们可以使用原始类型将这些值解组为 Go 中对应的数据类型:

numberJson := "3"
floatJson := "3.1412"
stringJson := `"bird"`

var n int
var pi float64
var str string

json.Unmarshal([]byte(numberJson), &n)
fmt.Println(n)
// 3

json.Unmarshal([]byte(floatJson), &pi)
fmt.Println(pi)
// 3.1412

json.Unmarshal([]byte(stringJson), &str)
fmt.Println(str)
// bird

 

JSON 结构标签 - 自定义字段名称


我们之前看到 Go 使用约定来确定映射 JSON 属性的属性名称。

虽然有时,我们需要一个与您的 JSON 数据中提供的属性名称不同的属性名称。例如,考虑以下数据:

{
  "birdType": "pigeon",
  "what it does": "likes to perch on rocks"
}


在这里,我们希望birdType 在我们的 Go 代码中保留为 Species 属性。我们也不可能为“它的作用”之类的键提供合适的属性名称。

为了解决这个问题,我们可以使用结构字段标签:

type Bird struct {
  Species string `json:"birdType"`
  Description string `json:"what it does"`
}


现在,我们可以明确地告诉我们的代码将哪个 JSON 属性映射到哪个属性。

birdJson := `{"birdType": "pigeon","what it does": "likes to perch on rocks"}`
var bird Bird
json.Unmarshal([]byte(birdJson), &bird)
fmt.Println(bird)
// {pigeon likes to perch on rocks}

将 JSON 解码为地图 - 非结构化数据


如果您事先不知道 JSON 属性的结构,则不能使用结构来解组数据。

相反,您可以使用地图。考虑一些 JSON 格式:

{
  "birds": {
    "pigeon":"likes to perch on rocks",
    "eagle":"bird of prey"
  },
  "animals": "none"
}


我们无法构建结构来表示所有情况下的上述数据,因为与鸟对应的键可以更改,这将改变结构。

为了处理这种情况,我们创建了一个字符串到空接口的映射:

birdJson := `{"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"},"animals":"none"}`
var result map[string]any
json.Unmarshal([]byte(birdJson), &result)

// The object stored in the "birds" key is also stored as 
// a map[string]any type, and its type is asserted from
// the `any` type
birds := result["birds"].(map[string]any)

for key, value := range birds {
  // Each value is an `any` type, that is type asserted as a string
  fmt.Println(key, value.(string))
}

 

每个字符串对应一个 JSON 属性,其映射的任意类型对应值,该值可以是任意类型。然后我们使用类型断言将任何类型转换为它的实际类型。
这些映射可以迭代,因此可以通过一个简单的 for 循环来处理未知数量的键。

验证 JSON 数据


在实际应用中,我们有时可能会得到无效(或不完整)的 JSON 数据。我们来看一个例子,其中部分数据被截断,生成的 JSON 字符串无效:

{
  "birds": {
    "pigeon":"likes to perch on rocks",
    "eagle":"bird of prey"


在实际应用中,这可能是由于网络错误或写入文件的数据不完整造成的

如果我们试图解组这个,我们的代码会恐慌:

birdJson := `{"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"`
var result map[string]any
json.Unmarshal([]byte(birdJson), &result)


输出:

panic: interface conversion: interface {} is nil, not map[string]interface {}

 

我们当然可以处理恐慌并从我们的代码中恢复,但这不是惯用的或可读的。

相反,我们可以使用 json.Valid 函数来检查我们的 JSON 数据的有效性:

if !json.Valid([]byte(birdJson)) {
    // handle the error here
    fmt.Println("invalid JSON string:", birdJson)
    return
}


现在,我们的代码将提前返回并给出输出:

invalid JSON string: {"birds":{"pigeon":"likes to perch on rocks","eagle":"bird of prey"


 

封送 JSON 数据


用于解码 JSON 字符串的相同规则也可以应用于编码。

编组结构化数据


让我们考虑之前的 Go 结构,并查看从其类型的数据中获取 JSON 字符串所需的代码:

package main

import (
    "encoding/json"
    "fmt"
)

// The same json tags will be used to encode data into JSON
type Bird struct {
    Species     string `json:"birdType"`
    Description string `json:"what it does"`
}

func main() {
    pigeon := &Bird{
        Species:     "Pigeon",
        Description: "likes to eat seed",
    }

    // we can use the json.Marhal function to
    // encode the pigeon variable to a JSON string
    data, _ := json.Marshal(pigeon)
    // data is the JSON string represented as bytes
    // the second parameter here is the error, which we
    // are ignoring for now, but which you should ideally handle
    // in production grade code

    // to print the data, we can typecast it to a string
    fmt.Println(string(data))
}

这将给出输出:

{"birdType":"Pigeon","what it does":"likes to eat seed"}


 

忽略空字段


在某些情况下,我们希望忽略 JSON 输出中的字段,如果它的值为空。为此,我们可以使用“omitempty”属性。

例如,如果鸽子对象的描述字段缺失,则在我们设置此属性的情况下,密钥将不会出现在编码的 JSON 字符串中:

package main

import (
    "encoding/json"
    "fmt"
)

type Bird struct {
    Species     string `json:"birdType"`
    // we can set the "omitempty" property as part of the JSON tag
    Description string `json:"what it does,omitempty"`
}

func main() {
    pigeon := &Bird{
        Species:     "Pigeon",
    }

    data, _ := json.Marshal(pigeon)

    fmt.Println(string(data))
}

这将为我们提供输出:

{"birdType":"Pigeon"}

 

如果我们想总是忽略一个字段,我们可以使用 json:"-" 结构标签来表示我们永远不希望包含这个字段:

package main

import (
    "encoding/json"
    "fmt"
)

type Bird struct {
    Species     string `json:"-"`
}

func main() {
    pigeon := &Bird{
        Species:     "Pigeon",
    }

    data, _ := json.Marshal(pigeon)

    fmt.Println(string(data))
}

此代码将始终打印一个空的 JSON 对象:

{}
 

编组切片


这与结构没有太大区别。我们只需要将切片或数组传递给 json.Marshal 函数,它就会像您期望的那样对数据进行编码:

pigeon := &Bird{
  Species:     "Pigeon",
  Description: "likes to eat seed",
}

// Now we pass a slice of two pigeons
data, _ := json.Marshal([]*Bird{pigeon, pigeon})
fmt.Println(string(data))

这将给出输出:

[{"birdType":"Pigeon","what it does":"likes to eat seed"},{"birdType":"Pigeon","what it does":"likes to eat seed"}]


 

编组地图


我们可以使用地图对非结构化数据进行编码。

map 的键需要是字符串,或者是可以转换为字符串的类型。这些值可以是任何可序列化的类型。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    // The keys need to be strings, the values can be
    // any serializable value
    birdData := map[string]any{
        "birdSounds": map[string]string{
            "pigeon": "coo",
            "eagle":  "squak",
        },
        "total birds": 2,
    }

    // JSON encoding is done the same way as before    
    data, _ := json.Marshal(birdData)
    fmt.Println(string(data))
}

输出:

{"birdSounds":{"eagle":"squak","pigeon":"coo"},"total birds":2}


最佳实践(结构与地图)


作为一般经验法则,如果您可以使用结构来表示您的 JSON 数据,那么您应该使用它们。 使用映射的唯一充分理由是由于数据中键或值的不确定性而无法使用结构。

如果我们使用映射,我们要么需要每个键具有相同的数据类型,要么使用泛型类型并稍后进行转换。

 

文章链接