MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go JSON的使用

2023-07-316.4k 阅读

Go 语言中 JSON 简介

在现代软件开发中,JSON(JavaScript Object Notation)已经成为数据交换的标准格式之一。它轻量级、易于阅读和编写,同时也易于机器解析和生成。Go 语言作为一门高效、简洁的编程语言,对 JSON 提供了强大且便捷的支持。

Go 标准库中的 encoding/json 包提供了处理 JSON 数据的功能。无论是将 Go 数据结构编码为 JSON 格式的字节切片,还是将 JSON 数据解码为 Go 数据结构,encoding/json 包都能轻松胜任。

基本数据类型的 JSON 编码与解码

编码

在 Go 中,要将基本数据类型编码为 JSON,我们可以使用 json.Marshal 函数。这个函数接受一个任意类型的参数,并返回一个 JSON 格式的字节切片和可能的错误。

下面是一个将字符串编码为 JSON 的示例:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    str := "Hello, JSON!"
    jsonStr, err := json.Marshal(str)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonStr))
}

在这个例子中,我们调用 json.Marshal 函数将字符串 str 编码为 JSON 格式的字节切片。如果编码过程中发生错误,我们打印错误信息并返回。否则,我们将字节切片转换为字符串并打印出来。

对于数字类型,编码过程类似:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    num := 42
    jsonNum, err := json.Marshal(num)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonNum))
}

布尔类型的编码也遵循相同的模式:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    b := true
    jsonB, err := json.Marshal(b)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonB))
}

解码

解码基本数据类型的 JSON 数据同样使用 encoding/json 包中的函数。json.Unmarshal 函数接受两个参数:一个是 JSON 格式的字节切片,另一个是指向要解码到的变量的指针。

以解码字符串为例:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonStr := []byte(`"Hello, decoded!"`)
    var str string
    err := json.Unmarshal(jsonStr, &str)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Println(str)
}

这里我们定义了一个 JSON 格式的字节切片 jsonStr,它包含一个字符串。我们使用 json.Unmarshal 函数将这个 JSON 字符串解码到一个 string 类型的变量 str 中。如果解码出错,打印错误信息。

对于数字类型的解码:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonNum := []byte(`42`)
    var num int
    err := json.Unmarshal(jsonNum, &num)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Println(num)
}

解码布尔类型:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonB := []byte(`true`)
    var b bool
    err := json.Unmarshal(jsonB, &b)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Println(b)
}

结构体与 JSON 的交互

结构体编码为 JSON

在实际应用中,我们经常需要将复杂的结构体编码为 JSON。Go 语言使得这个过程非常直观。

假设有一个表示用户信息的结构体:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city"`
}

func main() {
    user := User{
        Name: "Alice",
        Age:  30,
        City: "New York",
    }
    jsonUser, err := json.Marshal(user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUser))
}

在这个例子中,我们定义了一个 User 结构体,它有三个字段:NameAgeCity。每个字段后面跟着一个结构体标签,这里的标签 json:"name"json:"age"json:"city" 用于指定在 JSON 输出中字段的名称。当我们调用 json.Marshal 函数对 user 实例进行编码时,输出的 JSON 会使用这些标签指定的名称。

JSON 解码为结构体

将 JSON 数据解码为结构体同样简单。假设我们有一个 JSON 字符串表示用户信息,我们可以这样解码:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city"`
}

func main() {
    jsonStr := []byte(`{"name":"Bob","age":25,"city":"San Francisco"}`)
    var user User
    err := json.Unmarshal(jsonStr, &user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, City: %s\n", user.Name, user.Age, user.City)
}

这里我们定义了与编码时相同的 User 结构体,然后使用 json.Unmarshal 函数将 JSON 字节切片解码到 user 结构体实例中。如果解码成功,我们打印出用户的信息。

嵌套结构体的 JSON 处理

嵌套结构体编码

在实际场景中,结构体可能会包含其他结构体作为字段,形成嵌套结构。例如,我们有一个表示地址的结构体,并且在 User 结构体中包含这个地址结构体:

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
    Zip    string `json:"zip"`
}

type User struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
}

func main() {
    addr := Address{
        Street: "123 Main St",
        City:   "Anytown",
        Zip:    "12345",
    }
    user := User{
        Name:    "Charlie",
        Age:     35,
        Address: addr,
    }
    jsonUser, err := json.Marshal(user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUser))
}

在这个例子中,User 结构体包含一个 Address 结构体类型的字段 Address。当我们对 user 进行编码时,Address 结构体的字段也会被正确编码到 JSON 中,形成嵌套的 JSON 对象。

嵌套结构体解码

解码包含嵌套结构体的 JSON 数据也很直接。假设我们有如下 JSON 数据:

{
    "name": "David",
    "age": 40,
    "address": {
        "street": "456 Elm St",
        "city": "Othercity",
        "zip": "67890"
    }
}

对应的 Go 代码为:

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
    Zip    string `json:"zip"`
}

type User struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
}

func main() {
    jsonStr := []byte(`{"name":"David","age":40,"address":{"street":"456 Elm St","city":"Othercity","zip":"67890"}}`)
    var user User
    err := json.Unmarshal(jsonStr, &user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, Street: %s, City: %s, Zip: %s\n", user.Name, user.Age, user.Address.Street, user.Address.City, user.Address.Zip)
}

这里我们定义了与编码时相同的嵌套结构体 UserAddress,通过 json.Unmarshal 函数将 JSON 数据解码到 user 结构体实例中,并打印出相关信息。

切片与 JSON 的交互

切片编码为 JSON

切片在 Go 语言中非常常用,将切片编码为 JSON 也是很常见的操作。例如,我们有一个包含多个用户的切片,我们可以这样编码:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    users := []User{
        {Name: "Eve", Age: 28},
        {Name: "Frank", Age: 32},
    }
    jsonUsers, err := json.Marshal(users)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUsers))
}

在这个例子中,我们定义了一个 User 结构体,然后创建了一个 User 类型的切片 users。当我们调用 json.Marshal 函数对 users 切片进行编码时,输出的 JSON 是一个包含多个用户对象的数组。

JSON 解码为切片

解码 JSON 数组到切片也很容易。假设我们有如下 JSON 数据:

[
    {"name":"Grace","age":22},
    {"name":"Hank","age":27}
]

对应的 Go 代码为:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonStr := []byte(`[{"name":"Grace","age":22},{"name":"Hank","age":27}]`)
    var users []User
    err := json.Unmarshal(jsonStr, &users)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    for _, user := range users {
        fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
    }
}

这里我们定义了 User 结构体,然后使用 json.Unmarshal 函数将 JSON 数组解码到 users 切片中。通过遍历切片,我们可以访问每个用户的信息。

处理 JSON 中的空值

在 JSON 中,null 表示空值。在 Go 中,当解码 JSON 数据时,如果某个字段的值为 null,我们需要适当处理。

结构体字段与空值

对于结构体字段,我们可以使用指针类型来处理可能的空值。例如:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name *string `json:"name"`
    Age  *int    `json:"age"`
}

func main() {
    jsonStr := []byte(`{"name":"Ivy","age":null}`)
    var user User
    err := json.Unmarshal(jsonStr, &user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    if user.Name != nil {
        fmt.Println("Name:", *user.Name)
    }
    if user.Age != nil {
        fmt.Println("Age:", *user.Age)
    } else {
        fmt.Println("Age is nil")
    }
}

在这个例子中,User 结构体的 NameAge 字段都是指针类型。当 JSON 数据中的 age 字段为 null 时,解码后 user.Agenil,我们可以通过判断指针是否为 nil 来处理这种情况。

切片与空值

当处理包含可能为空值的 JSON 切片时,同样需要注意。例如,假设我们有如下 JSON 数据:

[
    {"name":"Jack","age":30},
    {"name":null,"age":35}
]

对应的 Go 代码:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name *string `json:"name"`
    Age  int     `json:"age"`
}

func main() {
    jsonStr := []byte(`[{"name":"Jack","age":30},{"name":null,"age":35}]`)
    var users []User
    err := json.Unmarshal(jsonStr, &users)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    for _, user := range users {
        if user.Name != nil {
            fmt.Println("Name:", *user.Name)
        } else {
            fmt.Println("Name is nil")
        }
        fmt.Println("Age:", user.Age)
    }
}

这里 User 结构体的 Name 字段为指针类型,以处理 JSON 中可能的 null 值。通过遍历切片,我们可以对每个用户的字段进行适当处理。

自定义 JSON 编码与解码行为

自定义编码

有时候,我们可能需要对结构体的编码行为进行自定义。Go 语言允许我们通过实现 Marshaler 接口来做到这一点。

假设我们有一个表示时间间隔的结构体,我们希望以特定格式编码为 JSON:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Duration struct {
    Duration time.Duration
}

func (d Duration) MarshalJSON() ([]byte, error) {
    seconds := int64(d.Duration.Seconds())
    return json.Marshal(seconds)
}

func main() {
    dur := Duration{Duration: 5 * time.Second}
    jsonDur, err := json.Marshal(dur)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonDur))
}

在这个例子中,Duration 结构体实现了 Marshaler 接口的 MarshalJSON 方法。在这个方法中,我们将 time.Duration 类型转换为秒数,并使用 json.Marshal 对秒数进行编码。这样,当我们对 Duration 结构体实例进行编码时,会使用我们自定义的编码逻辑。

自定义解码

类似地,我们可以通过实现 Unmarshaler 接口来自定义解码行为。假设我们接收到的 JSON 中时间间隔是以秒数表示的,我们希望解码为 time.Duration

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Duration struct {
    Duration time.Duration
}

func (d *Duration) UnmarshalJSON(data []byte) error {
    var seconds int64
    err := json.Unmarshal(data, &seconds)
    if err != nil {
        return err
    }
    d.Duration = time.Duration(seconds) * time.Second
    return nil
}

func main() {
    jsonStr := []byte(`60`)
    var dur Duration
    err := json.Unmarshal(jsonStr, &dur)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Println(dur.Duration)
}

在这个例子中,Duration 结构体实现了 Unmarshaler 接口的 UnmarshalJSON 方法。在这个方法中,我们先将 JSON 数据解码为秒数,然后将秒数转换为 time.Duration 类型。这样,当我们对包含秒数的 JSON 数据进行解码时,会使用我们自定义的解码逻辑。

JSON 编码选项

缩进格式化

默认情况下,json.Marshal 函数输出的 JSON 是紧凑格式的,没有缩进和空格。如果我们希望输出格式化后的 JSON,可以使用 json.MarshalIndent 函数。

例如:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    user := User{
        Name: "Kathy",
        Age:  24,
    }
    jsonUser, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUser))
}

在这个例子中,json.MarshalIndent 函数的第二个参数是前缀字符串,这里为空字符串;第三个参数是缩进字符串,这里为两个空格。这样输出的 JSON 就会有缩进,更易于阅读。

忽略零值字段

有时候,我们可能不希望输出值为零值(例如 0""false 等)的字段。可以通过结构体标签来实现这一点。

例如:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name,omitempty"`
    Age  int    `json:"age,omitempty"`
}

func main() {
    user := User{
        Name: "",
        Age:  0,
    }
    jsonUser, err := json.Marshal(user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUser))
}

在这个例子中,User 结构体的 NameAge 字段的结构体标签中都添加了 omitempty 选项。这样,当字段值为零值时,编码后的 JSON 中不会包含这些字段。

JSON 解码选项

忽略未知字段

在解码 JSON 数据时,如果 JSON 数据包含结构体中不存在的字段,默认情况下 json.Unmarshal 会报错。但是我们可以通过使用 json.Decoder 结构体并调用其 DisallowUnknownFields 方法来忽略未知字段。

例如:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonStr := []byte(`{"name":"Leo","age":29,"unknown_field":"value"}`)
    var user User
    decoder := json.NewDecoder(bytes.NewReader(jsonStr))
    decoder.DisallowUnknownFields()
    err := decoder.Decode(&user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

在这个例子中,我们使用 json.NewDecoder 创建一个解码器,并调用 DisallowUnknownFields 方法。这样,即使 JSON 数据中包含 unknown_field 这样的未知字段,解码也不会报错,而是忽略这些未知字段。

性能优化

减少内存分配

在处理大量 JSON 数据时,频繁的内存分配会影响性能。一种优化方法是重用缓冲区。例如,在编码时,可以预先分配足够大的字节切片来存储 JSON 数据,而不是让 json.Marshal 函数动态分配内存。

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    user := User{
        Name: "Mona",
        Age:  33,
    }
    buf := make([]byte, 256)
    n, err := json.Marshal(buf[:0], user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    buf = buf[:n]
    fmt.Println(string(buf))
}

在这个例子中,我们预先分配了一个大小为 256 的字节切片 buf。然后使用 json.Marshal 函数的变体形式,将 JSON 数据写入 buf 切片中,并根据返回的长度调整 buf 的实际使用长度。

并发处理

如果需要处理多个 JSON 编码或解码任务,可以考虑使用并发来提高性能。例如,假设有多个用户数据需要编码为 JSON,可以使用 Go 协程并发处理:

package main

import (
    "encoding/json"
    "fmt"
    "sync"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func encodeUser(user User, wg *sync.WaitGroup, resultChan chan []byte) {
    defer wg.Done()
    jsonUser, err := json.Marshal(user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    resultChan <- jsonUser
}

func main() {
    users := []User{
        {Name: "Nina", Age: 26},
        {Name: "Oscar", Age: 28},
    }
    var wg sync.WaitGroup
    resultChan := make(chan []byte, len(users))
    for _, user := range users {
        wg.Add(1)
        go encodeUser(user, &wg, resultChan)
    }
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    for jsonUser := range resultChan {
        fmt.Println(string(jsonUser))
    }
}

在这个例子中,我们定义了一个 encodeUser 函数,它在一个单独的协程中对 User 进行编码,并将结果发送到 resultChan 通道中。通过 sync.WaitGroup 来等待所有协程完成,然后关闭通道,最后从通道中读取并打印编码后的 JSON 数据。这样可以并发处理多个用户的编码任务,提高整体性能。

常见错误及解决方法

编码错误

  1. 结构体标签错误:如果结构体标签设置不正确,可能导致编码后的 JSON 字段名称不符合预期,或者某些字段被忽略。例如,标签拼写错误:
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"nam"` // 错误的标签
    Age  int    `json:"age"`
}

func main() {
    user := User{
        Name: "Paul",
        Age:  31,
    }
    jsonUser, err := json.Marshal(user)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonUser))
}

解决方法是仔细检查结构体标签,确保其正确性。

  1. 循环引用:如果结构体之间存在循环引用,编码时会导致 json.Marshal 函数陷入无限循环,最终导致程序崩溃。例如:
package main

import (
    "encoding/json"
    "fmt"
)

type Node struct {
    Value int    `json:"value"`
    Next  *Node  `json:"next"`
}

func main() {
    node1 := &Node{Value: 1}
    node2 := &Node{Value: 2}
    node1.Next = node2
    node2.Next = node1
    jsonNode, err := json.Marshal(node1)
    if err != nil {
        fmt.Println("编码错误:", err)
        return
    }
    fmt.Println(string(jsonNode))
}

解决方法是打破循环引用,例如可以使用指针间接引用或者在编码前对数据结构进行预处理,避免循环引用。

解码错误

  1. JSON 格式错误:如果 JSON 数据本身格式不正确,json.Unmarshal 函数会返回错误。例如:
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonStr := []byte(`{"name":"Quinn","age:32}`) // 错误的 JSON 格式,缺少引号
    var user User
    err := json.Unmarshal(jsonStr, &user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

解决方法是确保 JSON 数据格式正确,可以使用在线 JSON 校验工具进行验证。

  1. 类型不匹配:如果 JSON 数据中的字段类型与结构体字段类型不匹配,解码会失败。例如:
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    jsonStr := []byte(`{"name":"Rachel","age":"thirty - three"}`) // 错误的类型,age 应该是数字
    var user User
    err := json.Unmarshal(jsonStr, &user)
    if err != nil {
        fmt.Println("解码错误:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

解决方法是确保 JSON 数据的字段类型与结构体字段类型一致。如果无法避免类型差异,可以在解码前对 JSON 数据进行类型转换,或者在结构体中使用接口类型并在解码后进行类型断言和转换。

通过对以上内容的学习,你应该对 Go 语言中 JSON 的使用有了全面且深入的理解。无论是处理简单的基本数据类型,还是复杂的嵌套结构体和切片,Go 的 encoding/json 包都提供了丰富而强大的功能,并且通过合理的优化和错误处理,可以在实际项目中高效、稳定地使用 JSON 进行数据交换和存储。