第32课 - JSON 编解码
学习目标
- 理解 JSON(JavaScript Object Notation)数据格式的基本结构。
- 掌握使用
encoding/json标准库包进行 JSON 数据的序列化(Marshal)和反序列化(Unmarshal)。 - 学会使用结构体标签(Struct Tags)控制 JSON 键名和字段选项。
- 了解在处理 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 结构体:
- 将
Title在 JSON 中映射为"title"。 - 将
Content在 JSON 中映射为"body"。 - 如果
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!"}
]
}
- 定义与之匹配的 Go 结构体(使用合适的标签)。
- 将 JSON 字符串反序列化到该结构体。
- 打印出文章作者 (
info.author) 和第二条评论的内容 (comments[1].text)。 预期输出:
作者: Alice
第二条评论: Thanks!
常见错误
- 结构体字段未导出:Go 的 JSON 包只能访问和设置大写字母开头的导出字段。如果字段是小写开头(如
name string),它会被 JSON 编码器忽略,反序列化时也不会被填充。 - 标签拼写错误:标签语法必须严格正确,例如
`json:"name"`。常见的错误包括漏掉冒号、引号或使用了错误的标签名(如Json或JSon)。 - 类型不匹配:尝试将一个数字类型的 JSON 值(
"age": "25")反序列化到 Go 的int类型字段会导致错误。需要确保 Go 结构体字段的类型与 JSON 中值的类型兼容。 - 忘记处理错误:
json.Marshal和json.Unmarshal都会返回错误。务必检查err,否则程序可能在运行时出现难以追踪的 panic。 - 对零值的误解:默认情况下,零值(如
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...