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

Go encoding/json包使用的最佳实践

2022-03-292.5k 阅读

理解 JSON 与 Go 的 encoding/json 包

JSON 简介

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以易于阅读和编写的文本形式表示结构化数据,同时也易于机器解析和生成。它基于 JavaScript 的一个子集,广泛应用于 Web 应用程序的数据传输与存储,例如 RESTful API 数据交互、配置文件等场景。例如,一个简单的 JSON 对象如下:

{
    "name": "John",
    "age": 30,
    "city": "New York"
}

Go 的 encoding/json 包

Go 语言标准库中的 encoding/json 包提供了将 Go 数据结构编码为 JSON 格式以及将 JSON 数据解码为 Go 数据结构的功能。这个包非常高效且易于使用,是 Go 语言处理 JSON 数据的核心工具。在使用前,我们需要在代码中导入该包:

import (
    "encoding/json"
)

编码(Marshal):将 Go 数据结构转换为 JSON

基本类型的编码

Go 的基本类型,如 boolstringintfloat 等都可以很容易地编码为 JSON。例如,将一个 int 类型编码为 JSON:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    num := 42
    jsonData, err := json.Marshal(num)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

在上述代码中,json.Marshal 函数将 num 变量编码为 JSON 格式的字节切片。如果编码成功,将其转换为字符串后输出,这里输出的是 "42"。对于布尔类型,true 会编码为 truefalse 编码为 false;字符串类型会被包裹在双引号中。

结构体的编码

结构体是 Go 中常用的数据结构,将结构体编码为 JSON 也非常方便。假设我们有一个表示用户信息的结构体:

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

func main() {
    user := User{
        Name: "Alice",
        Age:  25,
        City: "San Francisco",
    }
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

在这个 User 结构体中,我们使用了结构体标签(struct tag)。json:"name" 这样的标签指定了在 JSON 输出中字段的名称。这里 Name 字段在 JSON 中会显示为 "name",而不是 Name。运行上述代码,输出为 {"name":"Alice","age":25,"city":"San Francisco"}

匿名字段的编码

当结构体包含匿名字段时,编码规则会稍有不同。例如:

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

type Company struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

func main() {
    company := Company{
        Name: "Tech Inc.",
        Address: Address{
            Street: "123 Main St",
            City:   "Anytown",
        },
    }
    jsonData, err := json.Marshal(company)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

这里 Company 结构体包含一个匿名字段 Address。在 JSON 输出中,Address 结构体的字段会作为 Company 结构体的直接子字段显示,输出为 {"name":"Tech Inc.","address":{"street":"123 Main St","city":"Anytown"}}

切片和映射的编码

切片的编码

切片在 JSON 中通常编码为数组。例如,一个整数切片的编码:

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    jsonData, err := json.Marshal(numbers)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

上述代码输出 [1,2,3,4,5]。如果是结构体切片,每个结构体元素会按照结构体编码规则进行编码。例如:

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    products := []Product{
        {Name: "Laptop", Price: 1000.0},
        {Name: "Mouse", Price: 50.0},
    }
    jsonData, err := json.Marshal(products)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

输出为 [{"name":"Laptop","price":1000},{"name":"Mouse","price":50}]

映射的编码

Go 中的映射在 JSON 中编码为对象。例如:

func main() {
    data := map[string]interface{}{
        "name": "Bob",
        "age":  40,
        "hobbies": []string{"reading", "swimming"},
    }
    jsonData, err := json.Marshal(data)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

这里使用了 map[string]interface{} 类型的映射,它可以存储不同类型的值。输出为 {"age":40,"hobbies":["reading","swimming"],"name":"Bob"}。注意,JSON 对象的字段顺序是不固定的,因此每次运行输出的字段顺序可能不同。

编码选项

缩进输出(格式化 JSON)

默认情况下,json.Marshal 输出的是紧凑格式的 JSON 字符串,没有空格和换行。如果需要格式化输出,以便于阅读,可以使用 json.MarshalIndent 函数。例如:

func main() {
    user := User{
        Name: "Charlie",
        Age:  35,
        City: "Los Angeles",
    }
    jsonData, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

json.MarshalIndent 的第二个参数是前缀,这里为空字符串;第三个参数是缩进字符串,这里使用两个空格。输出如下:

{
  "name": "Charlie",
  "age": 35,
  "city": "Los Angeles"
}

忽略零值字段

有时候我们不希望零值字段出现在 JSON 输出中。可以在结构体标签中使用 omitempty 选项。例如:

type Order struct {
    ID    int    `json:"id"`
    Items []Item `json:"items"`
    Total float64 `json:"total,omitempty"`
}

type Item struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    item1 := Item{Name: "Book", Price: 25.0}
    order := Order{
        ID:    1,
        Items: []Item{item1},
    }
    jsonData, err := json.Marshal(order)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

Order 结构体中,Total 字段标签中有 omitempty。由于 order 实例中的 Total 为零值(0.0),在 JSON 输出中不会包含 "total" 字段。输出为 {"id":1,"items":[{"name":"Book","price":25}]}

自定义编码行为

如果结构体的某个字段需要自定义编码行为,可以为该结构体实现 Marshaler 接口。该接口只有一个方法 MarshalJSON。例如,假设我们有一个表示日期的结构体,并且希望以特定格式编码为 JSON:

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

type Date struct {
    time.Time
}

func (d Date) MarshalJSON() ([]byte, error) {
    return []byte(`"` + d.Time.Format("2006-01-02") + `"`), nil
}

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

Date 结构体中,我们嵌入了 time.Time 类型,并实现了 MarshalJSON 方法,将日期格式化为 YYYY - MM - DD 的字符串形式。输出类似于 "2023 - 10 - 05"

解码(Unmarshal):将 JSON 转换为 Go 数据结构

基本类型的解码

将 JSON 字符串解码为 Go 的基本类型同样简单。例如,将一个 JSON 数字解码为 int

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

这里 json.Unmarshal 的第一个参数是 JSON 字符串的字节切片,第二个参数是指向目标变量的指针。运行代码,会输出 42。对于布尔类型和字符串类型的解码也类似,例如:

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

    strStr := `"Hello, World!"`
    var s string
    err = json.Unmarshal([]byte(strStr), &s)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(s)
}

结构体的解码

将 JSON 数据解码为结构体是常见的操作。假设我们有如下 JSON 数据:

{"name":"David","age":28,"city":"Boston"}

对应的 Go 结构体和解码代码如下:

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

func main() {
    jsonStr := `{"name":"David","age":28,"city":"Boston"}`
    var user User
    err := json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d, City: %s\n", user.Name, user.Age, user.City)
}

在这个例子中,JSON 字段名称与结构体标签中的名称对应,解码后结构体 user 会填充相应的值。

嵌套结构体的解码

当 JSON 数据包含嵌套结构时,对应的 Go 结构体也需要有嵌套结构。例如:

{
    "name": "Big Company",
    "address": {
        "street": "456 Elm St",
        "city": "Metropolis"
    }
}
type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}

type Company struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

func main() {
    jsonStr := `{"name":"Big Company","address":{"street":"456 Elm St","city":"Metropolis"}}`
    var company Company
    err := json.Unmarshal([]byte(jsonStr), &company)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Company: %s, Address: %s, %s\n", company.Name, company.Address.Street, company.Address.City)
}

这里 Company 结构体包含 Address 结构体作为匿名字段,解码时会正确填充嵌套的地址信息。

切片和映射的解码

切片的解码

将 JSON 数组解码为 Go 切片。例如,将 JSON 数组 [1, 2, 3, 4, 5] 解码为整数切片:

func main() {
    jsonStr := `[1, 2, 3, 4, 5]`
    var numbers []int
    err := json.Unmarshal([]byte(jsonStr), &numbers)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(numbers)
}

如果是结构体切片的解码,假设 JSON 数据如下:

[{"name":"Phone","price":500},{"name":"Tablet","price":300}]

对应的 Go 代码为:

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    jsonStr := `[{"name":"Phone","price":500},{"name":"Tablet","price":300}]`
    var products []Product
    err := json.Unmarshal([]byte(jsonStr), &products)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    for _, product := range products {
        fmt.Printf("Name: %s, Price: %.2f\n", product.Name, product.Price)
    }
}

映射的解码

将 JSON 对象解码为 Go 映射。例如:

func main() {
    jsonStr := `{"name":"Eve","age":32}`
    var data map[string]interface{}
    err := json.Unmarshal([]byte(jsonStr), &data)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    for key, value := range data {
        fmt.Printf("%s: %v\n", key, value)
    }
}

这里使用 map[string]interface{} 类型的映射来接收 JSON 对象,interface{} 可以存储任意类型的值。在实际应用中,需要根据 JSON 数据的具体结构进行类型断言,以获取正确类型的值。

解码选项

处理额外字段

默认情况下,json.Unmarshal 会忽略 JSON 数据中结构体中不存在的字段。但是,如果希望在遇到额外字段时返回错误,可以使用 json.Decoder 并调用 DisallowUnknownFields 方法。例如:

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

func main() {
    jsonStr := `{"name":"Frank","age":35,"job":"Engineer"}`
    var user User
    decoder := json.NewDecoder(strings.NewReader(jsonStr))
    decoder.DisallowUnknownFields()
    err := decoder.Decode(&user)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

在这个例子中,User 结构体没有 job 字段,由于调用了 DisallowUnknownFields,解码时会返回错误,提示遇到未知字段 "job"

自定义解码行为

如果结构体需要自定义解码行为,可以实现 Unmarshaler 接口,该接口有一个 UnmarshalJSON 方法。例如,对于之前的 Date 结构体,假设 JSON 中日期格式为 MM/DD/YYYY,我们可以如下实现自定义解码:

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

type Date struct {
    time.Time
}

func (d *Date) UnmarshalJSON(b []byte) error {
    s := string(b[1 : len(b)-1])
    t, err := time.Parse("01/02/2006", s)
    if err != nil {
        return err
    }
    d.Time = t
    return nil
}

func main() {
    jsonStr := `"10/05/2023"`
    var date Date
    err := json.Unmarshal([]byte(jsonStr), &date)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(date.Time.Format("2006-01-02"))
}

这里 UnmarshalJSON 方法从 JSON 字符串中提取日期部分,并使用 time.Parse 解析为 time.Time 类型,然后赋值给结构体中的 Time 字段。

常见问题与解决方法

类型不匹配问题

在编码和解码过程中,最常见的问题是类型不匹配。例如,将一个 JSON 字符串解码为不匹配的 Go 类型。假设 JSON 数据为 "hello",而我们尝试将其解码为 int

func main() {
    jsonStr := `"hello"`
    var num int
    err := json.Unmarshal([]byte(jsonStr), &num)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(num)
}

这里会返回错误 json: cannot unmarshal string into Go struct field .num of type int。解决方法是确保 JSON 数据类型与目标 Go 类型匹配。

编码和解码错误处理

在实际应用中,编码和解码操作可能会失败,因此正确处理错误非常重要。json.Marshaljson.Unmarshal 函数都会返回错误。例如,在编码时,如果结构体中包含不支持编码的类型(如 func 类型),会返回错误:

type Problem struct {
    Func func()
}

func main() {
    prob := Problem{}
    jsonData, err := json.Marshal(prob)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }
    fmt.Println(string(jsonData))
}

这里会返回错误 json: unsupported type: func()。在解码时,如果 JSON 数据格式不正确,也会返回错误。例如,JSON 字符串缺少引号:

func main() {
    jsonStr := `{name:"Grace", age:22}`
    var user User
    err := json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

会返回错误 invalid character 'n' looking for beginning of object key string。因此,在编码和解码操作后,始终要检查错误并进行适当处理。

性能优化

在处理大量 JSON 数据时,性能优化是很重要的。

减少内存分配

尽量复用已有的数据结构,避免在编码和解码过程中频繁分配内存。例如,在解码 JSON 数组为切片时,可以预先分配足够的容量:

func main() {
    jsonStr := `[1, 2, 3, 4, 5]`
    numbers := make([]int, 0, 5)
    err := json.Unmarshal([]byte(jsonStr), &numbers)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Println(numbers)
}

这样可以减少切片在增长过程中的内存重新分配。

使用 json.Decoderjson.Encoder

json.Decoderjson.Encoder 提供了更细粒度的控制和更好的性能。例如,在读取大型 JSON 文件时,可以使用 json.Decoder 逐块读取和解码,而不是一次性将整个文件读入内存再解码。

func main() {
    file, err := os.Open("large.json")
    if err != nil {
        fmt.Println("Open file error:", err)
        return
    }
    defer file.Close()

    decoder := json.NewDecoder(file)
    for decoder.More() {
        var data map[string]interface{}
        err := decoder.Decode(&data)
        if err != nil {
            fmt.Println("Decode error:", err)
            return
        }
        // 处理 data
    }
}

同样,在写入大量 JSON 数据时,json.Encoder 可以逐块编码并写入输出流,避免一次性分配大量内存。

func main() {
    file, err := os.Create("output.json")
    if err != nil {
        fmt.Println("Create file error:", err)
        return
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    data := []map[string]interface{}{
        {"name": "Item1", "value": 1},
        {"name": "Item2", "value": 2},
    }
    for _, item := range data {
        err := encoder.Encode(item)
        if err != nil {
            fmt.Println("Encode error:", err)
            return
        }
    }
}

通过合理使用这些方法,可以显著提高处理 JSON 数据的性能。

与其他库的结合使用

与 net/http 库结合

在构建 Web 应用程序时,经常需要在 HTTP 响应和请求中处理 JSON 数据。net/http 库与 encoding/json 包结合非常紧密。例如,在 HTTP 处理函数中返回 JSON 数据:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type Response struct {
    Message string `json:"message"`
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        resp := Response{Message: "Hello, World!"}
        jsonData, err := json.Marshal(resp)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.Write(jsonData)
    })
    fmt.Println("Server listening on :8080")
    http.ListenAndServe(":8080", nil)
}

在这个例子中,当客户端访问根路径时,服务器将 Response 结构体编码为 JSON 并返回。同时设置了 Content - Type 头为 application/json

接收 JSON 数据的 HTTP 请求也类似。例如:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)

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

func main() {
    http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) {
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        defer r.Body.Close()

        var data RequestData
        err = json.Unmarshal(body, &data)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        fmt.Printf("Name: %s, Age: %d\n", data.Name, data.Age)
        w.WriteHeader(http.StatusOK)
    })
    fmt.Println("Server listening on :8080")
    http.ListenAndServe(":8080", nil)
}

这里在 /submit 路径的处理函数中,读取 HTTP 请求体并将其解码为 RequestData 结构体。

与数据库操作结合

在开发数据库应用时,可能需要将数据库中的数据编码为 JSON 格式,或者将 JSON 数据解码后存储到数据库中。例如,使用 database/sql 库与 encoding/json 包结合。假设我们有一个简单的数据库表存储用户信息:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    data JSON NOT NULL
);

在 Go 中,将用户结构体编码为 JSON 并插入数据库:

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    _ "github.com/go - sql - driver/mysql"
)

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

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
    if err != nil {
        fmt.Println("Open database error:", err)
        return
    }
    defer db.Close()

    user := User{Name: "George", Age: 27}
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Marshal error:", err)
        return
    }

    stmt, err := db.Prepare("INSERT INTO users (data) VALUES (?)")
    if err != nil {
        fmt.Println("Prepare statement error:", err)
        return
    }
    defer stmt.Close()

    _, err = stmt.Exec(string(jsonData))
    if err != nil {
        fmt.Println("Execute statement error:", err)
        return
    }
    fmt.Println("User inserted successfully")
}

从数据库中读取 JSON 数据并解码为结构体:

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    _ "github.com/go - sql - driver/mysql"
)

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

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
    if err != nil {
        fmt.Println("Open database error:", err)
        return
    }
    defer db.Close()

    var jsonStr string
    err = db.QueryRow("SELECT data FROM users WHERE id = 1").Scan(&jsonStr)
    if err != nil {
        fmt.Println("Query error:", err)
        return
    }

    var user User
    err = json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("Unmarshal error:", err)
        return
    }
    fmt.Printf("Name: %s, Age: %d\n", user.Name, user.Age)
}

通过这样的方式,可以方便地在数据库和 JSON 数据格式之间进行转换。

通过以上对 encoding/json 包的详细介绍,包括编码、解码操作,各种选项,常见问题解决以及与其他库的结合使用,希望能帮助你在 Go 语言开发中更高效地处理 JSON 数据,编写出健壮、高性能的应用程序。无论是小型的命令行工具还是大型的 Web 应用,掌握这些最佳实践都将对你的开发工作大有裨益。