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

Go JSON序列化与反序列化

2021-11-215.4k 阅读

Go 语言中的 JSON 基础

在现代软件开发中,JSON(JavaScript Object Notation)已经成为一种广泛使用的数据交换格式。它以其简洁、易读的文本格式,在不同语言编写的系统之间轻松传递数据。Go 语言对 JSON 处理提供了强大且高效的支持,无论是将 Go 数据结构转换为 JSON 格式(序列化),还是将 JSON 数据解析回 Go 数据结构(反序列化)。

JSON 格式简介

JSON 数据主要由两种结构组成:对象和数组。对象是一个无序的键值对集合,键必须是字符串,值可以是字符串、数字、布尔值、null、对象或数组。数组是一个有序的值序列,值同样可以是上述提到的任意类型。例如,以下是一个简单的 JSON 对象:

{
    "name": "John",
    "age": 30,
    "isStudent": false,
    "hobbies": ["reading", "swimming"],
    "address": {
        "city": "New York",
        "country": "USA"
    }
}

在 Go 语言中,我们需要将这样的 JSON 数据映射到相应的结构体或其他数据类型,以便于处理。

Go 语言的 JSON 序列化

使用 encoding/json

Go 语言的标准库 encoding/json 包提供了用于 JSON 序列化和反序列化的函数。序列化是将 Go 数据结构转换为 JSON 格式的过程。最常用的函数是 json.Marshal,它接受一个 Go 值作为参数,并返回一个字节切片,其中包含该值的 JSON 编码。

基本类型的序列化

对于基本类型,如字符串、整数、布尔值等,序列化非常直接。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var str string = "Hello, JSON"
    data, err := json.Marshal(str)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(data))
}

在上述代码中,我们定义了一个字符串变量 str,然后使用 json.Marshal 对其进行序列化。如果序列化成功,data 将包含 JSON 格式的字节切片,我们将其转换回字符串并打印。这里,输出将是 "Hello, JSON",注意字符串两边的双引号是 JSON 格式要求的。

结构体的序列化

结构体是 Go 语言中常用的数据结构,将结构体序列化为 JSON 是非常常见的需求。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    IsStudent bool `json:"is_student"`
}

func main() {
    p := Person{
        Name:    "Alice",
        Age:     25,
        IsStudent: true,
    }
    data, err := json.Marshal(p)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(data))
}

在这段代码中,我们定义了一个 Person 结构体。结构体字段后面的反引号中的内容是结构体标签(struct tag)。这里的 json:"name"json:"age"json:"is_student" 标签用于指定在 JSON 序列化时字段的名称。如果没有这些标签,默认会使用结构体字段的大写名称,但 JSON 标准中通常使用小写字母和下划线命名。运行这段代码,输出将是 {"name":"Alice","age":25,"is_student":true}

嵌套结构体的序列化

当结构体中包含其他结构体作为字段时,同样可以轻松序列化。

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

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

func main() {
    a := Address{
        City:    "Shanghai",
        Country: "China",
    }
    p := Person{
        Name:    "Bob",
        Age:     32,
        Address: a,
    }
    data, err := json.Marshal(p)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(data))
}

这里 Person 结构体包含一个 Address 结构体类型的字段。序列化后的 JSON 数据将反映这种嵌套结构,输出为 {"name":"Bob","age":32,"address":{"city":"Shanghai","country":"China"}}

切片和映射的序列化

切片和映射在 Go 语言中也很常用,它们同样可以被序列化为 JSON。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    sliceData := []string{"apple", "banana", "cherry"}
    sliceBytes, err := json.Marshal(sliceData)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(sliceBytes))

    mapData := map[string]int{"one": 1, "two": 2, "three": 3}
    mapBytes, err := json.Marshal(mapData)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(mapBytes))
}

切片会被序列化为 JSON 数组,上述代码中 sliceData 序列化后的输出是 ["apple","banana","cherry"]。映射会被序列化为 JSON 对象,mapData 序列化后的输出是 {"one":1,"two":2,"three":3}

格式化输出

json.Marshal 生成的是紧凑格式的 JSON 数据,不包含空格和换行符,这在需要人类可读的场景下不太友好。encoding/json 包提供了 json.MarshalIndent 函数来生成格式化后的 JSON 数据。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    IsStudent bool `json:"is_student"`
}

func main() {
    p := Person{
        Name:    "Charlie",
        Age:     28,
        IsStudent: false,
    }
    data, err := json.MarshalIndent(p, "", "  ")
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(data))
}

json.MarshalIndent 的第二个参数是前缀,第三个参数是缩进字符串。在上述代码中,我们使用两个空格作为缩进,输出的 JSON 数据将是:

{
  "name": "Charlie",
  "age": 28,
  "is_student": false
}

Go 语言的 JSON 反序列化

使用 json.Unmarshal

反序列化是将 JSON 数据转换回 Go 数据结构的过程。encoding/json 包中的 json.Unmarshal 函数用于此目的。它接受两个参数,第一个是包含 JSON 数据的字节切片,第二个是指向目标 Go 值的指针。

基本类型的反序列化

对于基本类型的反序列化,要注意 JSON 数据类型和 Go 语言数据类型的匹配。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonStr := `"Hello from JSON"`
    var str string
    err := json.Unmarshal([]byte(jsonStr), &str)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(str)
}

这里我们将一个 JSON 字符串反序列化为 Go 字符串。注意,json.Unmarshal 的第一个参数必须是字节切片,所以我们使用 []byte(jsonStr) 进行转换。同时,第二个参数必须是指针,这样函数才能修改目标值。

结构体的反序列化

将 JSON 数据反序列化为结构体是常见的操作。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    IsStudent bool `json:"is_student"`
}

func main() {
    jsonStr := `{"name":"David","age":35,"is_student":false}`
    var p Person
    err := json.Unmarshal([]byte(jsonStr), &p)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, IsStudent: %v\n", p.Name, p.Age, p.IsStudent)
}

在这个例子中,我们定义了 Person 结构体,并将 JSON 字符串反序列化为 Person 结构体实例。结构体标签在这里同样重要,它帮助 json.Unmarshal 正确地将 JSON 字段映射到结构体字段。

嵌套结构体的反序列化

处理嵌套结构体的反序列化时,只要结构体定义正确,过程与普通结构体类似。

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

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

func main() {
    jsonStr := `{"name":"Eva","age":22,"address":{"city":"Paris","country":"France"}}`
    var p Person
    err := json.Unmarshal([]byte(jsonStr), &p)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, City: %s, Country: %s\n", p.Name, p.Age, p.Address.City, p.Address.Country)
}

这里 Person 结构体包含 Address 结构体,JSON 数据中的嵌套结构也能正确地反序列化为对应的 Go 结构体。

切片和映射的反序列化

反序列化 JSON 数组到 Go 切片以及 JSON 对象到 Go 映射也是常见的操作。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonArray := `["red", "green", "blue"]`
    var sliceData []string
    err := json.Unmarshal([]byte(jsonArray), &sliceData)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(sliceData)

    jsonObject := `{"one": 1, "two": 2, "three": 3}`
    var mapData map[string]int
    err = json.Unmarshal([]byte(jsonObject), &mapData)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(mapData)
}

在这个例子中,我们将 JSON 数组反序列化为字符串切片,将 JSON 对象反序列化为字符串到整数的映射。

处理 JSON 中的可选字段

在 JSON 数据中,某些字段可能是可选的,即数据中可能不存在这些字段。在 Go 结构体反序列化时,可以通过设置结构体字段的默认值和使用指针类型来处理这种情况。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Email   *string `json:"email,omitempty"`
}

func main() {
    jsonStr1 := `{"name":"Frank","age":27}`
    var p1 Person
    err := json.Unmarshal([]byte(jsonStr1), &p1)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, Email: %v\n", p1.Name, p1.Age, p1.Email)

    jsonStr2 := `{"name":"Grace","age":31,"email":"grace@example.com"}`
    var p2 Person
    err = json.Unmarshal([]byte(jsonStr2), &p2)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, Email: %v\n", p2.Name, p2.Age, p2.Email)
}

Person 结构体中,Email 字段是指针类型,并且标签中使用了 omitempty。当 JSON 数据中不存在 email 字段时,p1.Email 将为 nil;当存在时,p2.Email 将指向反序列化后的字符串值。

高级 JSON 处理技巧

自定义 Marshal 和 Unmarshal 方法

有时候,默认的序列化和反序列化行为不能满足需求,我们可以为结构体定义自定义的 MarshalJSONUnmarshalJSON 方法。

package main

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

type CustomTime struct {
    time.Time
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    // 自定义时间格式
    formated := ct.Time.Format("2006-01-02 15:04:05")
    return json.Marshal(formated)
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    var s string
    err := json.Unmarshal(data, &s)
    if err != nil {
        return err
    }
    t, err := time.Parse("2006-01-02 15:04:05", s)
    if err != nil {
        return err
    }
    ct.Time = t
    return nil
}

func main() {
    now := CustomTime{time.Now()}
    data, err := json.Marshal(now)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(data))

    var ct CustomTime
    err = json.Unmarshal([]byte(`"2023-10-01 12:34:56"`), &ct)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(ct.Time)
}

在这个例子中,我们定义了 CustomTime 结构体,它嵌入了 time.Time。通过实现 MarshalJSONUnmarshalJSON 方法,我们可以自定义时间的 JSON 序列化和反序列化格式。

使用 json.RawMessage

json.RawMessage 类型允许在结构体中存储未解析的 JSON 数据。这在需要延迟解析部分 JSON 数据或者处理结构不确定的 JSON 数据时非常有用。

package main

import (
    "encoding/json"
    "fmt"
)

type Container struct {
    Field1 string         `json:"field1"`
    Field2 json.RawMessage `json:"field2"`
}

func main() {
    jsonStr := `{"field1":"value1","field2":{"subfield":"subvalue"}}`
    var c Container
    err := json.Unmarshal([]byte(jsonStr), &c)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println("Field1:", c.Field1)
    fmt.Println("Field2:", string(c.Field2))

    var subData map[string]string
    err = json.Unmarshal(c.Field2, &subData)
    if err != nil {
        fmt.Println("Unmarshal sub data error:", err)
        return
    }
    fmt.Println("Subfield:", subData["subfield"])
}

Container 结构体中,Field2json.RawMessage 类型。首先,我们将 JSON 数据反序列化为 Container 结构体,Field2 中的 JSON 数据被存储为字节切片。然后,我们可以根据需要进一步将 Field2 反序列化为具体的 Go 数据结构。

处理 JSON 流

在处理大量 JSON 数据或者需要逐块处理 JSON 数据时,可以使用 json.Decoderjson.Encoder 来处理 JSON 流。

package main

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

func main() {
    jsonStream := strings.NewReader(`{"name":"Hank","age":40} {"name":"Ivy","age":29}`)
    decoder := json.NewDecoder(jsonStream)
    for {
        var person map[string]interface{}
        err := decoder.Decode(&person)
        if err != nil {
            break
        }
        fmt.Println(person)
    }
}

这里我们使用 strings.NewReader 创建一个模拟的 JSON 数据流。json.NewDecoder 从这个流中读取数据,并逐块将其反序列化为 map[string]interface{}。在实际应用中,jsonStream 可能来自网络连接、文件等数据源。

处理 JSON 中的特殊值

JSON 中有一些特殊值,如 null。在 Go 语言中,处理 null 值需要特别注意。例如,在反序列化时,如果 JSON 中的某个字段是 null,而对应的 Go 结构体字段是基本类型,会导致反序列化错误。可以使用指针类型或者 interface{} 类型来处理 null 值。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string      `json:"name"`
    Age     *int        `json:"age"`
    Details interface{} `json:"details"`
}

func main() {
    jsonStr := `{"name":"Jack","age":null,"details":{"note":"Some note"}}`
    var p Person
    err := json.Unmarshal([]byte(jsonStr), &p)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %v, Details: %v\n", p.Name, p.Age, p.Details)
}

在这个例子中,Age 字段是指针类型,当 JSON 中的 agenull 时,p.Age 将为 nilDetails 字段使用 interface{} 类型,可以存储任何 JSON 值,包括 null

性能优化与注意事项

性能优化

  1. 预分配内存:在序列化和反序列化切片和映射时,预分配足够的内存可以减少内存重新分配的次数,提高性能。例如,在反序列化一个已知长度的 JSON 数组到切片时,可以预先分配切片的容量。
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonArray := `["item1","item2","item3"]`
    var sliceData []string
    // 预先分配容量
    sliceData = make([]string, 0, 3)
    err := json.Unmarshal([]byte(jsonArray), &sliceData)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(sliceData)
}
  1. 减少反射使用:虽然 Go 语言的 encoding/json 包内部使用反射来处理结构体的序列化和反序列化,但在可能的情况下,可以通过自定义 MarshalJSONUnmarshalJSON 方法来减少反射的使用。反射操作相对较慢,直接处理数据可以提高性能。

注意事项

  1. 字段命名和标签:在结构体序列化和反序列化时,字段命名和结构体标签非常重要。确保 JSON 字段名和结构体字段标签一致,否则可能导致数据映射错误。同时,注意标签中的 omitempty 等选项的使用,避免意外的数据丢失或错误。
  2. 数据类型匹配:在反序列化时,要确保 JSON 数据类型和目标 Go 数据类型匹配。例如,不能将 JSON 字符串反序列化为整数类型,否则会导致反序列化错误。在处理数值类型时,还要注意 JSON 中的数值范围和 Go 语言对应类型的范围是否匹配。
  3. 错误处理:在序列化和反序列化过程中,始终要进行错误处理。json.Marshaljson.Unmarshal 等函数可能会返回错误,如 JSON 格式错误、数据类型不匹配等。及时处理这些错误可以避免程序出现意外行为。

通过深入理解和掌握 Go 语言中 JSON 序列化与反序列化的原理、方法以及高级技巧,开发者可以更加高效地处理 JSON 数据,构建稳定、高性能的应用程序。无论是在 Web 开发、微服务架构还是数据处理等领域,这些知识都具有重要的实用价值。