Go 语言 JSON 解析与生成:实战教学
在现代网络应用中,JSON (JavaScript Object Notation) 已经成为一种主流的数据交换格式。它轻量、易读、易写,并且机器解析和生成效率高。Go 语言通过其标准库 encoding/json 提供了对 JSON 的出色内置支持,使得在 Go 应用程序中处理 JSON 数据变得非常便捷。
本文将详细介绍 Go 语言中 JSON 的编码(Marshaling)和解码(Unmarshaling)过程,并涵盖各种实战技巧,帮助您在 Go 项目中高效地处理 JSON 数据。
1. JSON 编码 (Marshaling)
将 Go 语言的结构体(struct)或其他 Go 值转换为 JSON 格式的字节切片([]byte)的过程称为编码或“Marshaling”。Go 语言主要使用 json.Marshal 函数来完成此操作。
基本用法:json.Marshal
json.Marshal 函数接收一个 Go 接口类型的值,并尝试将其转换为 JSON 格式的字节切片。需要注意的是,只有结构体中首字母大写的(即导出的)字段才会被编码到 JSON 输出中。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
)
type Person struct {
Name string // 导出的字段
Age int // 导出的字段
Email string // 导出的字段
}
func main() {
// 创建一个 Go 结构体实例
p := Person{
Name: “Alice”,
Age: 30,
Email: “[email protected]”,
}
// 将结构体编码为 JSON
jsonData, err := json.Marshal(p)
if err != nil {
log.Fatalf("编码到 JSON 失败: %v", err)
}
// 打印 JSON 数据(字节切片需要转换为字符串)
fmt.Println("原始 JSON:", string(jsonData))
// 使用 json.MarshalIndent 美化输出
prettyJSON, err := json.MarshalIndent(p, "", " ") // 第一个空字符串为前缀,第二个为缩进字符串
if err != nil {
log.Fatalf("带缩进编码 JSON 失败: %v", err)
}
fmt.Println("美化后的 JSON:")
fmt.Println(string(prettyJSON))
}
“`
在上述示例中,Person 结构体的 Name、Age 和 Email 字段都是导出的,因此它们都会被 json.Marshal 包含在最终的 JSON 输出中。json.MarshalIndent 提供了一个方便的方式来生成带有缩进的、更易读的 JSON 格式。
2. JSON 解码 (Unmarshaling)
将 JSON 格式的字节切片解析并存储到 Go 语言的结构体或 Go 值中的过程称为解码或“Unmarshaling”。Go 语言主要使用 json.Unmarshal 函数来完成此操作。
基本用法:json.Unmarshal
json.Unmarshal 函数接收一个 JSON 格式的字节切片和一个指向目标 Go 变量的指针。它会解析 JSON 数据,并将结果存储在指定的变量中。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
)
type Person struct {
Name string
Age int
Email string
}
func main() {
// JSON 数据字符串
jsonString := {"Name":"Bob","Age":25,"Email":"[email protected]"}
// 创建一个空的 Person 结构体实例,用于接收解码后的数据
var p Person
// 将 JSON 数据解码到结构体中
err := json.Unmarshal([]byte(jsonString), &p) // 注意这里需要传入 p 的地址
if err != nil {
log.Fatalf("解码 JSON 失败: %v", err)
}
// 打印解码后的结构体内容
fmt.Printf("解码后的 Person: 姓名: %s, 年龄: %d, 邮箱: %s\n", p.Name, p.Age, p.Email)
// 解码到 map[string]interface{}(动态类型)
// 当 JSON 结构不确定或高度动态时,可以解码到 map
var data map[string]interface{}
err = json.Unmarshal([]byte(jsonString), &data)
if err != nil {
log.Fatalf("解码 JSON 到 map 失败: %v", err)
}
fmt.Println("解码到 map:", data)
}
“`
在不确定 JSON 数据结构时,将 JSON 解码到 map[string]interface{} 是一种灵活的做法。interface{} 类型允许存储任何类型的值,但在访问数据时需要进行类型断言。
3. 处理不同类型的 JSON 数据
encoding/json 包能够处理各种 JSON 类型,包括数组、嵌套对象、布尔值、数字和字符串。您只需定义相应的 Go 类型来匹配 JSON 结构。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
)
// 地址结构体
type Address struct {
Street string json:"street" // 自定义 JSON 字段名
City string json:"city"
ZipCode string json:"zip_code" // 自定义 JSON 字段名
}
// 员工结构体,包含嵌套结构体和数组
type Employee struct {
ID int json:"id"
FirstName string json:"first_name"
LastName string json:"last_name"
IsActive bool json:"is_active"
Addresses []Address json:"addresses" // 地址数组
Metadata map[string]string json:"metadata" // 任意键值对的 Map
}
func main() {
// — 编码复杂类型 —
employee := Employee{
ID: 101,
FirstName: “John”,
LastName: “Doe”,
IsActive: true,
Addresses: []Address{ // 嵌套结构体数组
{Street: “123 Main St”, City: “Anytown”, ZipCode: “12345”},
{Street: “456 Oak Ave”, City: “Otherville”, ZipCode: “67890”},
},
Metadata: map[string]string{ // Map
“department”: “Engineering”,
“level”: “Senior”,
},
}
jsonData, err := json.MarshalIndent(employee, "", " ")
if err != nil {
log.Fatalf("编码员工信息失败: %v", err)
}
fmt.Println("编码后的员工信息:")
fmt.Println(string(jsonData))
// --- 解码复杂类型 ---
jsonString := `{
"id": 202,
"first_name": "Jane",
"last_name": "Smith",
"is_active": false,
"addresses": [
{"street": "789 Pine Ln", "city": "Somewhere", "zip_code": "11223"}
],
"metadata": {
"project": "Alpha",
"status": "On Leave"
}
}`
var decodedEmployee Employee
err = json.Unmarshal([]byte(jsonString), &decodedEmployee)
if err != nil {
log.Fatalf("解码员工 JSON 失败: %v", err)
}
fmt.Println("\n解码后的员工信息:")
fmt.Printf("ID: %d, 姓名: %s %s, 活跃状态: %t\n",
decodedEmployee.ID, decodedEmployee.FirstName, decodedEmployee.LastName, decodedEmployee.IsActive)
for i, addr := range decodedEmployee.Addresses {
fmt.Printf(" 地址 %d: %s, %s %s\n", i+1, addr.Street, addr.City, addr.ZipCode)
}
fmt.Println(" 元数据:", decodedEmployee.Metadata)
}
“`
此示例展示了如何将包含嵌套结构体(Address)、结构体数组(Addresses []Address)以及 Map(Metadata map[string]string)的 Go 结构体与 JSON 互相转换。
4. 自定义 JSON 字段名和 omitempty
Go 结构体标签(struct tags)允许您自定义字段在 JSON 编码/解码时的行为。json 标签是专门用于此目的的。
json:"field_name": 指定字段在 JSON 中的键名。json:"-": 在编码和解码时完全忽略该字段。json:"field_name,omitempty": 如果字段的值是其类型的零值(例如,int的0,string的"",指针/切片/Map 的nil,bool的false),则在编码时省略该字段。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
)
type Product struct {
ID int json:"product_id" // 自定义字段名
Name string json:"product_name" // 自定义字段名
Price float64 json:"price,omitempty" // 如果为零值则省略
Description string json:"-" // 忽略此字段
Category string json:"category,omitempty" // 如果为零值则省略
InStock bool json:"in_stock"
Supplier *string json:"supplier,omitempty" // 如果为 nil 则省略
}
func main() {
// 示例 1: 所有字段都存在
supplier1 := “Acme Corp”
p1 := Product{
ID: 1,
Name: “Laptop”,
Price: 1200.50,
Description: “Powerful computing device”,
Category: “Electronics”,
InStock: true,
Supplier: &supplier1,
}
jsonData1, err := json.MarshalIndent(p1, “”, ” “)
if err != nil {
log.Fatalf(“编码 p1 失败: %v”, err)
}
fmt.Println(“产品 1 (所有字段):”)
fmt.Println(string(jsonData1))
// 示例 2: 包含零值或被忽略的字段
p2 := Product{
ID: 2,
Name: "Mouse",
Price: 0, // 会因 omitempty 被省略
Description: "Wireless mouse",
InStock: false,
Supplier: nil, // 会因 omitempty 被省略
}
jsonData2, err := json.MarshalIndent(p2, "", " ")
if err != nil {
log.Fatalf("编码 p2 失败: %v", err)
}
fmt.Println("\n产品 2 (omitempty 效果):")
fmt.Println(string(jsonData2))
// --- 使用标签进行解码 ---
jsonString := `{
"product_id": 3,
"product_name": "Keyboard",
"price": 75.99,
"description": "Mechanical keyboard",
"category": "Peripherals",
"in_stock": true,
"supplier": "Logitech"
}`
var p3 Product
err = json.Unmarshal([]byte(jsonString), &p3)
if err != nil {
log.Fatalf("解码 p3 失败: %v", err)
}
fmt.Println("\n解码后的产品 3:")
// 注意 Description 字段由于 `json:"-"` 被忽略,所以其值将保持零值
supplierVal := "nil"
if p3.Supplier != nil {
supplierVal = *p3.Supplier
}
fmt.Printf("ID: %d, 名称: %s, 价格: %.2f, 描述: \"%s\", 类别: \"%s\", 有库存: %t, 供应商: %v\n",
p3.ID, p3.Name, p3.Price, p3.Description, p3.Category, p3.InStock, supplierVal)
}
“`
在 p2 的编码结果中,price 和 supplier 字段由于 omitempty 标签而没有出现在 JSON 中。而 Description 字段无论是否有值,都会因为 json:"-" 标签被忽略。
5. 处理未知字段
默认情况下,json.Unmarshal 会忽略 JSON 中没有对应导出字段的字段。如果您希望严格检查 JSON 输入,确保没有意外的字段,可以使用 (*json.Decoder).DisallowUnknownFields()。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
“strings”
)
type SimpleProduct struct {
Name string json:"name"
Price float64 json:"price"
}
func main() {
// 包含未知字段 “color” 的 JSON
jsonStringWithUnknown := {"name":"Book","price":20.00,"color":"red"}
var p SimpleProduct
// 默认行为: 未知字段会被忽略
err := json.Unmarshal([]byte(jsonStringWithUnknown), &p)
if err != nil {
log.Fatalf("默认解码失败: %v", err)
}
fmt.Println("默认解码 (未知字段被忽略):", p) // 输出: {Book 20}
// 严格解码: 不允许未知字段
readerWithUnknown := strings.NewReader(jsonStringWithUnknown)
decoderWithUnknown := json.NewDecoder(readerWithUnknown)
decoderWithUnknown.DisallowUnknownFields() // 禁用未知字段
var pStrict SimpleProduct
err = decoderWithUnknown.Decode(&pStrict)
if err != nil {
// 预期会报错: json: unknown field "color"
fmt.Printf("严格解码 (预期报错): %v\n", err)
} else {
fmt.Println("严格解码 (意外成功):", pStrict)
}
// 对有效 JSON 进行严格解码
validJsonString := `{"name":"Pen","price":5.00}`
readerValid := strings.NewReader(validJsonString)
decoderValid := json.NewDecoder(readerValid)
decoderValid.DisallowUnknownFields()
var pValid SimpleProduct
err = decoderValid.Decode(&pValid)
if err != nil {
log.Fatalf("严格解码有效 JSON 失败: %v", err)
}
fmt.Println("严格解码 (有效 JSON):", pValid) // 输出: {Pen 5}
}
“`
通过调用 decoder.DisallowUnknownFields(),可以强制解码器在遇到目标结构体中不存在的字段时返回错误,这有助于防止因 JSON 数据结构变化而引发的潜在问题。
6. JSON 流处理 (Streaming JSON)
当处理大型 JSON 数据或从网络流中读取/写入 JSON 时,使用 json.Encoder 和 json.Decoder 更为高效,因为它们直接操作 io.Writer 和 io.Reader 接口,避免了一次性将所有数据加载到内存中。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
“os”
“strings”
)
type Event struct {
Name string json:"name"
Details string json:"details"
Timestamp int64 json:"timestamp"
}
func main() {
// — 编码到流 (例如,os.Stdout) —
fmt.Println(“— 编码到标准输出 —“)
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent(“”, ” “) // 设置缩进以美化输出到流
event1 := Event{
Name: "UserLoggedIn",
Details: "User 'john.doe' logged in from 192.168.1.100",
Timestamp: 1678886400,
}
event2 := Event{
Name: "ItemAddedToCart",
Details: "Item 'SKU123' added by user 'jane.smith'",
Timestamp: 1678886460,
}
err := encoder.Encode(event1)
if err != nil {
log.Fatalf("编码 event1 失败: %v", err)
}
err = encoder.Encode(event2)
if err != nil {
log.Fatalf("编码 event2 失败: %v", err)
}
// --- 从流中解码 (例如,字符串读取器) ---
fmt.Println("\n--- 从字符串流解码 ---")
jsonStream := `
{"name":"UserSignedUp","details":"New user 'testuser' registered","timestamp":1678886500}
{"name":"OrderPlaced","details":"Order #XYZ789 placed by 'testuser'","timestamp":1678886550}
`
// 在实际应用中,这会是一个 io.Reader,如 os.Stdin 或网络连接
decoder := json.NewDecoder(strings.NewReader(jsonStream))
var e Event
for i := 1; decoder.More(); i++ { // decoder.More() 检查流中是否还有更多 JSON 对象
err := decoder.Decode(&e)
if err != nil {
log.Fatalf("解码事件 %d 失败: %v", i, err)
}
fmt.Printf("解码事件 %d: Name=%s, Details=%s, Timestamp=%d\n", i, e.Name, e.Details, e.Timestamp)
}
}
“`
json.NewEncoder 和 json.NewDecoder 分别创建了写入器和读取器,它们可以逐个处理 JSON 对象,而不是一次性处理整个 JSON 文档,这对于处理连续的 JSON 数据流非常有用。
7. 错误处理
在处理 JSON 时,始终检查 json.Marshal、json.Unmarshal、json.Encoder.Encode 和 json.Decoder.Decode 返回的错误是至关重要的。健壮的应用程序离不开良好的错误处理。
常见的错误类型包括:
*json.InvalidUnmarshalError: 如果向Unmarshal传入了非指针或nil指针,则会发生此错误。*json.UnmarshalTypeError: 如果 JSON 值与 Go 类型不匹配(例如,将字符串解码为整数),则会发生此错误。json.SyntaxError: 如果 JSON 输入格式不正确,则会发生此错误。*json.UnsupportedTypeError: 如果尝试编码不支持的 Go 类型(例如,通道chan),则会发生此错误。
“`go
package main
import (
“encoding/json”
“fmt”
“log”
)
type Item struct {
Name string json:"item_name"
Price int json:"price"
}
func main() {
// — 解码错误 —
fmt.Println(“— 解码错误 —“)
// 1. 格式错误的 JSON
malformedJSON := `{"item_name":"Book","price":100,}` // 尾部多余逗号
var item1 Item
err := json.Unmarshal([]byte(malformedJSON), &item1)
if err != nil {
fmt.Printf("错误 (格式错误的 JSON): %v\n", err)
if syntaxErr, ok := err.(*json.SyntaxError); ok {
fmt.Printf(" 语法错误偏移量 %d: %s\n", syntaxErr.Offset, syntaxErr.Error())
}
}
// 2. 类型不匹配
typeMismatchJSON := `{"item_name":"Laptop","price":"expensive"}` // price 是字符串,应为整数
var item2 Item
err = json.Unmarshal([]byte(typeMismatchJSON), &item2)
if err != nil {
fmt.Printf("错误 (类型不匹配): %v\n", err)
if typeErr, ok := err.(*json.UnmarshalTypeError); ok {
fmt.Printf(" 类型错误: 值 '%s' (类型 %s) 无法赋值给字段 %s (类型 %s)\n",
typeErr.Value, typeErr.JSONType, typeErr.Field, typeErr.Type)
}
}
// 3. 无效的解码目标 (非指针)
validJSON := `{"item_name":"Pen","price":5}`
var item3 Item // 非指针
err = json.Unmarshal([]byte(validJSON), item3) // 传入的是值而不是指针
if err != nil {
fmt.Printf("错误 (无效的解码目标): %v\n", err)
if invalidErr, ok := err.(*json.InvalidUnmarshalError); ok {
fmt.Printf(" 无效的解码错误: %s\n", invalidErr.Error())
}
}
// --- 编码错误 ---
fmt.Println("\n--- 编码错误 ---")
// 4. 不支持的类型 (例如,通道)
type BadStruct struct {
ID int
MyChan chan int // 通道不能被编码
}
bad := BadStruct{ID: 1, MyChan: make(chan int)}
err = json.Marshal(bad)
if err != nil {
fmt.Printf("错误 (不支持的类型): %v\n", err)
if unsupportedErr, ok := err.(*json.UnsupportedTypeError); ok {
fmt.Printf(" 不支持的类型错误: %s\n", unsupportedErr.Error())
}
}
}
“`
通过对错误进行类型断言,您可以获取更详细的错误信息,从而更好地调试和处理各种 JSON 相关的异常情况。
总结
Go 语言的 encoding/json 包为 JSON 数据的处理提供了强大而灵活的工具。无论是简单的结构体转换,还是复杂的嵌套数据结构、自定义字段名、零值省略,甚至是高效的流式处理,Go 都能轻松应对。掌握这些技巧将使您在开发 Go 应用程序时更加得心应手,能够高效且健壮地处理 JSON 数据。