32·标准库入门

JSON 编解码

jsonmarshalunmarshaltag

第32课 - JSON 编解码

学习目标

  1. 理解 JSON(JavaScript Object Notation)数据格式的基本结构。
  2. 掌握使用 encoding/json 标准库包进行 JSON 数据的序列化(Marshal)和反序列化(Unmarshal)。
  3. 学会使用结构体标签(Struct Tags)控制 JSON 键名和字段选项。
  4. 了解在处理 JSON 时常见的错误和最佳实践。

核心概念

JSON 是现代 Web 开发和数据交换中最常用的数据格式。它轻量、易于阅读和编写,并且被绝大多数编程语言支持。在 Go 语言中,我们主要使用标准库中的 encoding/json 包来处理 JSON 数据。

序列化(Marshal):将 Go 语言中的数据结构(如结构体、切片、映射)转换为 JSON 格式的字节切片([]byte)。这个过程就像把 Go 的东西“翻译”成 JSON。

反序列化(Unmarshal):将 JSON 格式的字节切片解析并填充到 Go 语言的数据结构中。这是序列化的逆过程。

结构体标签(Struct Tags):在定义结构体时,我们可以给字段附加特殊的注释(如 `json:"name"`),用来告诉 json 包在编码或解码时如何处理这个字段,例如,指定 JSON 中的键名,或者是否在值为空时忽略该字段。

代码示例

1. 基础编解码

package main

import (
	"encoding/json"
	"fmt"
)

// 定义一个用户结构体
type User struct {
	Name    string
	Age     int
	Email   string
	IsAdmin bool
}

func main() {
	// 创建一个用户实例
	user := User{
		Name:    "张三",
		Age:     28,
		Email:   "[email protected]",
		IsAdmin: false,
	}

	// 1. 序列化:将 User 结构体转换为 JSON
	jsonData, err := json.Marshal(user)
	if err != nil {
		fmt.Println("序列化错误:", err)
		return
	}
	// jsonData 是一个 []byte,我们将其转换为字符串以便查看
	fmt.Printf("JSON 输出: %s\n", string(jsonData))
	// 输出: {"Name":"张三","Age":28,"Email":"[email protected]","IsAdmin":false}

	// 2. 反序列化:将 JSON 字符串解析回 Go 结构体
	jsonStr := `{"Name":"李四","Age":32,"Email":"[email protected]","IsAdmin":true}`
	var newUser User
	err = json.Unmarshal([]byte(jsonStr), &newUser)
	if err != nil {
		fmt.Println("反序列化错误:", err)
		return
	}
	fmt.Printf("解析结果: %+v\n", newUser)
	// 输出: 解析结果: {Name:李四 Age:32 Email:[email protected] IsAdmin:true}
}

2. 使用结构体标签(Struct Tags)

package main

import (
	"encoding/json"
	"fmt"
)

type Product struct {
	ID    int     `json:"product_id"`       // 在JSON中键名为 "product_id"
	Name  string  `json:"name"`             // 键名为 "name"
	Price float64 `json:"price,omitempty"`  // 键名为 "price",如果值为0则在JSON中忽略
	SKU   string  `json:"-"`                // 整个字段在JSON序列化/反序列化时被忽略
}

func main() {
	// 测试忽略零值和特定字段
	product := Product{
		ID:    1001,
		Name:  "无线鼠标",
		Price: 0, // 价格为0,将被忽略
		SKU:   "MOUSE-XYZ",
	}

	jsonData, err := json.MarshalIndent(product, "", "  ") // 使用缩进美化输出
	if err != nil {
		fmt.Println("错误:", err)
		return
	}
	fmt.Println("美化后的 JSON:")
	fmt.Println(string(jsonData))
	// 输出:
	// {
	//   "product_id": 1001,
	//   "name": "无线鼠标"
	// }

	// 测试反序列化带标签的 JSON
	jsonStr := `{"product_id": 1002, "name": "机械键盘", "price": 599.99}`
	var p Product
	json.Unmarshal([]byte(jsonStr), &p)
	fmt.Printf("\n反序列化结果: %+v\n", p)
	// 输出: 反序列化结果: {ID:1002 Name:机械键盘 Price:599.99 SKU:}
}

3. 处理动态或嵌套的 JSON

当 JSON 结构未知或非常复杂时,可以使用 map[string]interface{}json.RawMessage

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	// 1. 使用 map[string]interface{} 解析未知结构
	unknownJSON := `{
		"name": "系统配置",
		"version": 2,
		"features": ["logging", "metrics"],
		"database": {
			"host": "localhost",
			"port": 5432
		}
	}`

	var result map[string]interface{}
	err := json.Unmarshal([]byte(unknownJSON), &result)
	if err != nil {
		fmt.Println("错误:", err)
		return
	}

	fmt.Printf("配置名称: %v\n", result["name"])
	// 访问嵌套的 map
	if db, ok := result["database"].(map[string]interface{}); ok {
		fmt.Printf("数据库端口: %v\n", db["port"])
	}

	// 2. 使用 json.RawMessage 进行延迟解析
	type Event struct {
		ID      int               `json:"id"`
		Type    string            `json:"type"`
		Payload json.RawMessage   `json:"payload"` // 原始的 JSON 字节
	}

	eventJSON := `{
		"id": 1,
		"type": "user_login",
		"payload": {"user_id": 123, "ip": "192.168.1.1"}
	}`

	var event Event
	json.Unmarshal([]byte(eventJSON), &event)

	fmt.Printf("\n事件类型: %s\n", event.Type)
	fmt.Printf("原始 Payload: %s\n", string(event.Payload))

	// 根据 Type 决定如何解析 Payload
	if event.Type == "user_login" {
		var loginData struct {
			UserID int    `json:"user_id"`
			IP     string `json:"ip"`
		}
		json.Unmarshal(event.Payload, &loginData)
		fmt.Printf("登录用户ID: %d, IP: %s\n", loginData.UserID, loginData.IP)
	}
}

实践练习

练习 1:基础序列化

定义一个表示 BlogPost 的结构体,包含 Title (string), Content (string), Tags ([]string), Published (bool) 字段。创建一个实例并将其序列化为美观(带缩进)的 JSON 字符串输出。 预期输出:

{
  "Title": "Go语言入门",
  "Content": "今天学习JSON...",
  "Tags": ["golang", "json", "tutorial"],
  "Published": true
}

练习 2:使用结构体标签

改进练习1的 BlogPost 结构体:

  1. Title 在 JSON 中映射为 "title"
  2. Content 在 JSON 中映射为 "body"
  3. 如果 Tags 为空切片([]string{}),则在 JSON 中忽略它。 预期输出 (当 Tags 为空时):
{
  "title": "Go语言入门",
  "body": "今天学习JSON...",
  "Published": true
}

练习 3:反序列化与嵌套

给定以下 JSON 字符串:

{
  "id": 1,
  "info": {
    "author": "Alice",
    "date": "2023-10-01"
  },
  "comments": [
    {"user": "Bob", "text": "Great!"},
    {"user": "Charlie", "text": "Thanks!"}
  ]
}
  1. 定义与之匹配的 Go 结构体(使用合适的标签)。
  2. 将 JSON 字符串反序列化到该结构体。
  3. 打印出文章作者 (info.author) 和第二条评论的内容 (comments[1].text)。 预期输出:
作者: Alice
第二条评论: Thanks!

常见错误

  1. 结构体字段未导出:Go 的 JSON 包只能访问和设置大写字母开头的导出字段。如果字段是小写开头(如 name string),它会被 JSON 编码器忽略,反序列化时也不会被填充。
  2. 标签拼写错误:标签语法必须严格正确,例如 `json:"name"`。常见的错误包括漏掉冒号、引号或使用了错误的标签名(如 JsonJSon)。
  3. 类型不匹配:尝试将一个数字类型的 JSON 值("age": "25")反序列化到 Go 的 int 类型字段会导致错误。需要确保 Go 结构体字段的类型与 JSON 中值的类型兼容。
  4. 忘记处理错误json.Marshaljson.Unmarshal 都会返回错误。务必检查 err,否则程序可能在运行时出现难以追踪的 panic。
  5. 对零值的误解:默认情况下,零值(如 0, "", false, nil)也会被序列化到 JSON 中。如果希望省略它们,需要使用 omitempty 标签。

小结

本节课我们学习了 Go 语言中处理 JSON 数据的核心方法:

  • json.Marshal:将 Go 数据编码为 JSON 字节。
  • json.Unmarshal:将 JSON 字节解码到 Go 变量中。
  • 结构体标签:是控制 JSON 键名和行为(如忽略字段或零值)的关键工具。
  • 对于结构不确定的 JSON,可以使用 map[string]interface{}json.RawMessage 进行灵活处理。 记住,处理错误和确保字段可导出是编写健壮 JSON 处理代码的基础。掌握这些,你就能轻松应对大部分 Web API 交互和数据持久化场景了。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「XML 与其他编码」 以巩固所学知识。