Go 从 0 到精通 · 第 22 课:JSON 编解码

学习定位:这是整套 Go 教程的第 22 课。
前置要求:已经完成第 21 课,掌握了字符串处理与常用工具库。需要熟悉结构体、切片、map 和错误处理。
本课目标:掌握 Go 中 JSON 数据的编码(序列化)与解码(反序列化),理解结构体标签的作用,能处理已知结构和未知结构的 JSON,能处理接口响应、配置文件和数据交换场景。


1. 本课你要解决的核心问题

JSON 是当今最流行的数据交换格式。后端 API 返回 JSON,前端发请求附带 JSON,配置文件用 JSON,日志也可能是 JSON。

你需要搞明白以下问题:

  • 怎么把 Go 的结构体变成 JSON 字符串(编码)
  • 怎么把 JSON 字符串变成 Go 的结构体(解码)
  • 结构体字段名和 JSON 键名不一样怎么办
  • 怎么处理不确定结构的 JSON
  • 怎么处理 JSON 中的嵌套对象、数组、空值

学完这一课,你就能在 Go 中自如地处理 JSON 数据了。


2. encoding/json 包总览

Go 处理 JSON 的所有功能都在 encoding/json 包里。核心函数就这几个:

函数 方向 用途
json.Marshal Go → JSON 把 Go 值编码为 JSON 字节
json.MarshalIndent Go → JSON 同上,但带缩进格式化
json.Unmarshal JSON → Go 把 JSON 字节解码为 Go 值
json.NewEncoder Go → JSON 流式编码(写入 io.Writer)
json.NewDecoder JSON → Go 流式解码(读取 io.Reader)

先记住这五个,下面逐步展开。


3. 编码:Go → JSON

3.1 最基本的编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"encoding/json"
"fmt"
)

type User struct {
Name string
Age int
Email string
}

func main() {
u := User{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
}

data, err := json.Marshal(u)
if err != nil {
fmt.Println("编码失败:", err)
return
}

fmt.Println(string(data))
}

输出:

1
{"Name":"Alice","Age":25,"Email":"alice@example.com"}

注意:输出的键名就是结构体的字段名(首字母大写),这就是默认行为。

3.2 带缩进的编码

json.Marshal 输出的是紧凑的一行 JSON。如果想要格式化的、带缩进的输出:

1
2
data, err := json.MarshalIndent(u, "", "  ")
fmt.Println(string(data))

输出:

1
2
3
4
5
{
"Name": "Alice",
"Age": 25,
"Email": "alice@example.com"
}

MarshalIndent 的三个参数:

  • 第一个:要编码的值
  • 第二个:每行的前缀(通常用 ""
  • 第三个:缩进字符串(通常用 " ""\t"

3.3 编码基本类型

json.Marshal 不只能编码结构体,任何 Go 值都可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 整数
data, _ := json.Marshal(42)
fmt.Println(string(data)) // 42

// 字符串
data, _ = json.Marshal("hello")
fmt.Println(string(data)) // "hello"

// 布尔值
data, _ = json.Marshal(true)
fmt.Println(string(data)) // true

// 切片
data, _ = json.Marshal([]int{1, 2, 3})
fmt.Println(string(data)) // [1,2,3]

// map
data, _ = json.Marshal(map[string]int{
"apple": 3,
"banana": 5,
})
fmt.Println(string(data)) // {"apple":3,"banana":5}

3.4 编码切片

1
2
3
4
5
6
7
users := []User{
{Name: "Alice", Age: 25, Email: "alice@example.com"},
{Name: "Bob", Age: 30, Email: "bob@example.com"},
}

data, _ := json.MarshalIndent(users, "", " ")
fmt.Println(string(data))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"Name": "Alice",
"Age": 25,
"Email": "alice@example.com"
},
{
"Name": "Bob",
"Age": 30,
"Email": "bob@example.com"
}
]

3.5 编码 map

1
2
3
4
5
6
7
8
9
config := map[string]interface{}{
"host": "localhost",
"port": 8080,
"debug": true,
"maxRetry": 3,
}

data, _ := json.MarshalIndent(config, "", " ")
fmt.Println(string(data))

输出:

1
2
3
4
5
6
{
"debug": true,
"host": "localhost",
"maxRetry": 3,
"port": 8080
}

注意:map 的 JSON 输出中键是排序的(按字母顺序),这样输出结果更稳定。


4. 结构体标签(json:"..."

4.1 为什么需要标签

上一节输出的 JSON 键名是 NameAge,但大多数 API 的 JSON 键名是小写的 nameage。怎么控制输出的键名?用结构体标签。

4.2 基本标签用法

1
2
3
4
5
6
7
8
9
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}

u := User{Name: "Alice", Age: 25, Email: "alice@example.com"}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))

输出:

1
2
3
4
5
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
}

在结构体字段后面用反引号包裹 json:"...",里面的字符串就是 JSON 中的键名。

4.3 忽略某个字段

如果不想让某个字段出现在 JSON 中:

1
2
3
4
5
6
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Password string `json:"-"` // 永远不出现在 JSON 中
internal string // 未导出字段也不会出现在 JSON 中
}

json:"-" 表示"忽略这个字段"。未导出的字段(首字母小写)默认就被忽略。

1
2
3
4
5
6
7
8
u := User{
Name: "Alice",
Age: 25,
Password: "secret123",
internal: "hidden",
}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))

输出:

1
2
3
4
{
"name": "Alice",
"age": 25
}

Passwordinternal 都不会出现在 JSON 中。

4.4 omitempty:空值时忽略

1
2
3
4
5
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}

加上 omitempty 后,如果字段是零值(空字符串、0、false、nil),就不会出现在 JSON 中:

1
2
3
u := User{Name: "Alice", Age: 25, Email: ""}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))

输出:

1
2
3
4
{
"name": "Alice",
"age": 25
}

Email 是空字符串,被 omitempty 忽略了。

各类型的零值:

类型 零值 是否被 omitempty 忽略
string ""
int 0
float64 0.0
bool false
slice nil
map nil
pointer nil

重要提醒omitempty 会把 0 也忽略掉。如果字段是 int 类型且 0 是有效值,不要用 omitempty

4.5 标签的完整语法

1
json:"key_name,omitempty,string"

可以组合多个选项,用逗号分隔:

  • 第一个:键名
  • omitempty:空值时忽略
  • string:将值编码为 JSON 字符串
1
2
3
4
5
type Config struct {
Port int `json:"port,string"` // 输出为 "8080"(字符串)
Debug bool `json:"debug,string"` // 输出为 "true"(字符串)
Timeout int `json:"timeout,omitempty"` // 0 时忽略
}

5. 解码:JSON → Go

5.1 最基本的解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"encoding/json"
"fmt"
)

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

func main() {
jsonStr := `{"name":"Alice","age":25,"email":"alice@example.com"}`

var u User
err := json.Unmarshal([]byte(jsonStr), &u)
if err != nil {
fmt.Println("解码失败:", err)
return
}

fmt.Println(u)
}

输出:

1
{Alice 25 alice@example.com}

注意Unmarshal 的第二个参数必须是指针&u),因为函数需要修改原来的变量。

5.2 解码为 map

如果你不知道 JSON 的结构,或者只想临时提取某些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jsonStr := `{"name":"Alice","age":25,"city":"Beijing"}`

var data map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
fmt.Println("解码失败:", err)
return
}

fmt.Println(data)
// map[age:25 city:Beijing name:Alice]

// 访问具体值(注意类型断言)
fmt.Println(data["name"].(string)) // Alice
fmt.Println(data["age"].(float64)) // 25(注意:数字解码为 float64!)

5.3 JSON 中数字的解码问题

这是新手最容易踩的坑:JSON 中的数字解码为 interface{} 时,会变成 float64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jsonStr := `{"count": 42}`
var data map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data)

// 直接断言为 int 会 panic
// count := data["count"].(int) // panic!

// 必须断言为 float64
count := data["count"].(float64)
fmt.Println(count) // 42
fmt.Printf("%T\n", count) // float64

// 如果需要 int,手动转换
intCount := int(count)
fmt.Println(intCount) // 42

如果知道 JSON 的结构,直接用结构体解码就没有这个问题——结构体字段的类型是确定的。

5.4 JSON 数组的解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jsonStr := `[
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]`

var users []User
err := json.Unmarshal([]byte(jsonStr), &users)
if err != nil {
fmt.Println("解码失败:", err)
return
}

for _, u := range users {
fmt.Println(u)
}
// {Alice 25 }
// {Bob 30 }

5.5 解码时的字段匹配规则

Go 在解码 JSON 到结构体时,匹配规则如下:

  1. 精确匹配 JSON 键和结构体标签
  2. 大小写不敏感匹配 JSON 键和结构体字段名
  3. 忽略 JSON 中有但结构体中没有的键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}

// JSON 中的键名大小写不匹配也能解码
jsonStr := `{"Name":"Alice","AGE":25,"email":"alice@example.com"}`
var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {Alice 25 alice@example.com}

// JSON 中多出来的键会被忽略
jsonStr2 := `{"name":"Alice","age":25,"city":"Beijing"}`
json.Unmarshal([]byte(jsonStr2), &u)
fmt.Println(u) // {Alice 25 }
// city 键被忽略,因为 User 结构体中没有对应字段

5.6 解码嵌套结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}

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

jsonStr := `{
"name": "Alice",
"age": 25,
"address": {
"city": "Beijing",
"country": "China"
}
}`

var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u)
// {Alice 25 {Beijing China}}
fmt.Println(u.Address.City) // Beijing

5.7 解码嵌套 JSON 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Tag struct {
Name string `json:"name"`
Color string `json:"color"`
}

type Article struct {
Title string `json:"title"`
Tags []Tag `json:"tags"`
}

jsonStr := `{
"title": "Go JSON 教程",
"tags": [
{"name": "go", "color": "blue"},
{"name": "json", "color": "green"}
]
}`

var article Article
json.Unmarshal([]byte(jsonStr), &article)
fmt.Println(article.Title) // Go JSON 教程
fmt.Println(article.Tags[0].Name) // go
fmt.Println(article.Tags[1].Color) // green

6. 处理未知结构的 JSON

真实场景中,你经常不知道 JSON 长什么样,或者 JSON 的结构是动态的。这时候有几种处理方式。

6.1 用 map[string]interface{}

最直接的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jsonStr := `{
"name": "Alice",
"age": 25,
"scores": [95, 88, 92],
"address": {
"city": "Beijing",
"zip": "100000"
}
}`

var data map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data)

// 取顶层值
fmt.Println(data["name"]) // Alice

// 取嵌套对象(需要类型断言)
address := data["address"].(map[string]interface{})
fmt.Println(address["city"]) // Beijing

// 取数组
scores := data["scores"].([]interface{})
fmt.Println(scores[0]) // 95

问题:到处都是类型断言,代码很脆弱。JSON 结构稍有变化就会 panic。

6.2 用 json.RawMessage 延迟解析

json.RawMessage 是一种"先把原始 JSON 存起来,稍后再解析"的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Response struct {
Status int `json:"status"`
Message string `json:"message"`
Data json.RawMessage `json:"data"` // 先不解析
}

jsonStr := `{
"status": 200,
"message": "success",
"data": {"name": "Alice", "age": 25}
}`

var resp Response
json.Unmarshal([]byte(jsonStr), &resp)

fmt.Println(resp.Status) // 200
fmt.Println(resp.Message) // success
fmt.Println(string(resp.Data)) // {"name": "Alice", "age": 25}

// 现在根据具体情况再解析 Data
type UserData struct {
Name string `json:"name"`
Age int `json:"age"`
}
var ud UserData
json.Unmarshal(resp.Data, &ud)
fmt.Println(ud) // {Alice 25}

这种方式在 API 返回多种类型的 data 时非常有用——先看 statusmessage,再决定怎么解析 data

6.3 遍历未知 JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func exploreJSON(data interface{}, indent int) {
prefix := ""
for i := 0; i < indent; i++ {
prefix += " "
}

switch v := data.(type) {
case map[string]interface{}:
for key, val := range v {
fmt.Printf("%s%s:\n", prefix, key)
exploreJSON(val, indent+1)
}
case []interface{}:
for i, val := range v {
fmt.Printf("%s[%d]:\n", prefix, i)
exploreJSON(val, indent+1)
}
case string:
fmt.Printf("%s\"%s\"\n", prefix, v)
case float64:
fmt.Printf("%s%.0f\n", prefix, v)
case bool:
fmt.Printf("%s%v\n", prefix, v)
case nil:
fmt.Printf("%snull\n", prefix)
}
}

func main() {
jsonStr := `{
"name": "Alice",
"age": 25,
"scores": [95, 88, 92],
"address": {
"city": "Beijing"
}
}`

var data interface{}
json.Unmarshal([]byte(jsonStr), &data)
exploreJSON(data, 0)
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name:
"Alice"
age:
25
scores:
[0]:
95
[1]:
88
[2]:
92
address:
city:
"Beijing"

7. 流式编解码

当 JSON 数据很大(比如几 MB 的文件),或者数据来自网络流时,用 Encoder/Decoder 更合适。

7.1 json.NewEncoder:流式编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"encoding/json"
"os"
)

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

func main() {
users := []User{
{Name: "Alice", Age: 25, Email: "alice@example.com"},
{Name: "Bob", Age: 30, Email: "bob@example.com"},
}

// 直接写入标准输出
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(users)
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
},
{
"name": "Bob",
"age": 30,
"email": "bob@example.com"
}
]

Encoder.Encode 会自动在末尾加换行。它接收任何 io.Writer,所以可以写入文件、网络连接、缓冲区等。

7.2 json.NewDecoder:流式解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

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

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

func main() {
// 模拟一个来自网络的 JSON 流
jsonStream := `[
{"name": "Alice", "age": 25, "email": "alice@example.com"},
{"name": "Bob", "age": 30, "email": "bob@example.com"}
]`

decoder := json.NewDecoder(strings.NewReader(jsonStream))

var users []User
err := decoder.Decode(&users)
if err != nil {
fmt.Println("解码失败:", err)
return
}

for _, u := range users {
fmt.Println(u)
}
}

7.3 逐条解码 JSON 数组

如果 JSON 数组非常大,不想一次性全部加载到内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
jsonStream := `[
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
{"name": "Charlie", "age": 28}
]`

decoder := json.NewDecoder(strings.NewReader(jsonStream))

// 先读取数组的开始标记 [
token, _ := decoder.Token()
fmt.Println(token) // [

// 逐条读取数组元素
for decoder.More() {
var u User
decoder.Decode(&u)
fmt.Println(u)
}

// 读取数组的结束标记 ]
token, _ = decoder.Token()
fmt.Println(token) // ]

8. 实战综合示例

8.1 配置文件读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

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

type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Debug bool `json:"debug"`
Timeout int `json:"timeout_seconds"`
Headers map[string]string `json:"headers"`
}

// LoadConfig 从文件加载配置
func LoadConfig(filename string) (*ServerConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}

var config ServerConfig
err = json.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}

return &config, nil
}

// SaveConfig 保存配置到文件
func SaveConfig(filename string, config *ServerConfig) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("编码配置失败: %w", err)
}

err = os.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}

return nil
}

func main() {
// 创建配置
config := &ServerConfig{
Host: "0.0.0.0",
Port: 8080,
Debug: true,
Timeout: 30,
Headers: map[string]string{
"Content-Type": "application/json",
"X-Api-Version": "v1",
},
}

// 保存
err := SaveConfig("server.json", config)
if err != nil {
fmt.Println("保存失败:", err)
return
}
fmt.Println("配置已保存")

// 加载
loaded, err := LoadConfig("server.json")
if err != nil {
fmt.Println("加载失败:", err)
return
}
fmt.Printf("加载的配置: %+v\n", loaded)
}

8.2 API 响应处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

import (
"encoding/json"
"fmt"
)

// APIResponse 模拟 API 返回的标准格式
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}

// User 用户数据
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

// Order 订单数据
type Order struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
}

func handleResponse(jsonStr string) {
var resp APIResponse
err := json.Unmarshal([]byte(jsonStr), &resp)
if err != nil {
fmt.Println("解析响应失败:", err)
return
}

fmt.Printf("状态码: %d, 消息: %s\n", resp.Code, resp.Message)

// 根据状态码决定怎么解析 data
if resp.Code != 200 {
fmt.Println("请求失败,无数据")
return
}

// 先尝试解析为用户
var user User
if err := json.Unmarshal(resp.Data, &user); err == nil && user.Name != "" {
fmt.Printf("用户: ID=%d, Name=%s, Email=%s\n", user.ID, user.Name, user.Email)
return
}

// 再尝试解析为订单
var order Order
if err := json.Unmarshal(resp.Data, &order); err == nil && order.ID != 0 {
fmt.Printf("订单: ID=%d, 金额=%.2f, 状态=%s\n", order.ID, order.Amount, order.Status)
return
}

fmt.Println("未知数据类型")
}

func main() {
// 模拟不同的 API 响应
responses := []string{
`{"code": 200, "message": "success", "data": {"id": 1, "name": "Alice", "email": "alice@example.com"}}`,
`{"code": 200, "message": "success", "data": {"id": 1001, "user_id": 1, "amount": 99.5, "status": "paid"}}`,
`{"code": 404, "message": "not found", "data": null}`,
}

for i, resp := range responses {
fmt.Printf("--- 响应 %d ---\n", i+1)
handleResponse(resp)
fmt.Println()
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
--- 响应 1 ---
状态码: 200, 消息: success
用户: ID=1, Name=Alice, Email=alice@example.com

--- 响应 2 ---
状态码: 200, 消息: success
订单: ID=1001, 金额=99.50, 状态=paid

--- 响应 3 ---
状态码: 404, 消息: not found
请求失败,无数据

8.3 数据库查询结果序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"encoding/json"
"fmt"
)

type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Tags []string `json:"tags,omitempty"`
InStock bool `json:"in_stock"`
Category string `json:"category,omitempty"`
}

func main() {
// 模拟数据库查询结果
products := []Product{
{ID: 1, Name: "Go 编程指南", Price: 59.9, Tags: []string{"编程", "Go"}, InStock: true, Category: "图书"},
{ID: 2, Name: "机械键盘", Price: 299.0, Tags: []string{"外设"}, InStock: true},
{ID: 3, Name: "显示器", Price: 1999.0, InStock: false},
}

// 序列化为 JSON 返回给前端
data, _ := json.MarshalIndent(products, "", " ")
fmt.Println(string(data))
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"id": 1,
"name": "Go 编程指南",
"price": 59.9,
"tags": ["编程", "Go"],
"in_stock": true,
"category": "图书"
},
{
"id": 2,
"name": "机械键盘",
"price": 299,
"tags": ["外设"],
"in_stock": true
},
{
"id": 3,
"name": "显示器",
"price": 1999,
"in_stock": false
}
]

注意第三条数据:没有 tags(因为是 nil 被 omitempty 忽略了),没有 category(因为是空字符串被 omitempty 忽略了)。


9. 自定义编解码

9.1 实现 json.Marshaler 接口

有时候默认的编码行为不能满足需求,比如你想把某个类型编码成特殊格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

// Duration 自定义时间间隔,以 "XhYmZs" 格式输出
type Duration struct {
time.Duration
}

// MarshalJSON 自定义编码
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}

type Task struct {
Name string `json:"name"`
Duration Duration `json:"duration"`
}

func main() {
task := Task{
Name: "写 Go 教程",
Duration: Duration{2*time.Hour + 30*time.Minute},
}

data, _ := json.MarshalIndent(task, "", " ")
fmt.Println(string(data))
}

输出:

1
2
3
4
{
"name": "写 Go 教程",
"duration": "2h30m0s"
}

如果不自定义,Duration 会被编码为一个数字(纳秒数),非常不直观。

9.2 实现 json.Unmarshaler 接口

对应地,自定义解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// UnmarshalJSON 自定义解码
func (d *Duration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
d.Duration = dur
return nil
}

这样就能从 "2h30m0s" 字符串正确解析回 Duration

9.3 实战:自定义时间格式

1
2
3
4
5
6
7
8
9
10
11
12
13
type Event struct {
Name string `json:"name"`
StartTime time.Time `json:"start_time"` // 默认编码是 RFC3339
}

e := Event{
Name: "Go 语言分享会",
StartTime: time.Date(2024, 6, 15, 14, 30, 0, 0, time.Local),
}

data, _ := json.MarshalIndent(e, "", " ")
fmt.Println(string(data))
// 输出: "start_time": "2024-06-15T14:30:00+08:00"

默认的 time.Time 编码格式是 RFC3339。如果你想要其他格式,可以自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type CustomTime struct {
time.Time
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Format("2006-01-02 15:04:05"))
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); 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
}

type Event struct {
Name string `json:"name"`
StartTime CustomTime `json:"start_time"`
}

e := Event{
Name: "Go 语言分享会",
StartTime: CustomTime{time.Date(2024, 6, 15, 14, 30, 0, 0, time.Local)},
}

data, _ := json.MarshalIndent(e, "", " ")
fmt.Println(string(data))
// "start_time": "2024-06-15 14:30:00"

10. 常见坑总结

10.1 Unmarshal 必须传指针

1
2
3
4
5
6
7
// 错误:没有传指针,数据不会被修改
var u User
json.Unmarshal([]byte(jsonStr), u) // 会报错或数据为空

// 正确:传指针
var u User
json.Unmarshal([]byte(jsonStr), &u) // 正确

这是最常见的错误。Unmarshal 需要修改传入的值,所以必须传指针。

10.2 数字解码为 float64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jsonStr := `{"price": 99.9}`
var data map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data)

// 错误:直接断言为 int
price := data["price"].(int) // panic!

// 正确:断言为 float64
price := data["price"].(float64) // 99.9

// 或者用结构体解码
type Product struct {
Price float64 `json:"price"`
}
var p Product
json.Unmarshal([]byte(jsonStr), &p)
// p.Price 直接就是 float64,不需要断言

建议:尽量用结构体而不是 map[string]interface{},避免类型断言问题。

10.3 omitempty 把 0 和 false 也忽略了

1
2
3
4
5
6
7
8
type Config struct {
Port int `json:"port,omitempty"`
Debug bool `json:"debug,omitempty"`
}

config := Config{Port: 0, Debug: false}
data, _ := json.Marshal(config)
fmt.Println(string(data)) // {} (空对象!Port 和 Debug 都被忽略了)

如果 0false 是有效值,不要用 omitempty

解决办法:用指针类型:

1
2
3
4
5
6
7
8
9
10
type Config struct {
Port *int `json:"port,omitempty"`
Debug *bool `json:"debug,omitempty"`
}

port := 0
debug := false
config := Config{Port: &port, Debug: &debug}
data, _ := json.Marshal(config)
fmt.Println(string(data)) // {"port":0,"debug:false}

10.4 未导出字段不会被编解码

1
2
3
4
5
6
7
8
9
type User struct {
Name string `json:"name"`
password string `json:"password"` // 首字母小写,未导出
}

u := User{Name: "Alice", password: "secret"}
data, _ := json.Marshal(u)
fmt.Println(string(data)) // {"name":"Alice"}
// password 不会出现在 JSON 中

JSON 包只能访问导出的字段(首字母大写)。

10.5 循环引用导致无限递归

1
2
3
4
5
6
7
8
9
10
11
type Node struct {
Name string `json:"name"`
Children []*Node `json:"children"`
Parent *Node `json:"parent"` // 可能导致循环引用
}

parent := &Node{Name: "root"}
child := &Node{Name: "child", Parent: parent}
parent.Children = []*Node{child}

// json.Marshal(parent) 会报错:json: unsupported value: encountered a cycle via *main.Node

解决办法:不编码导致循环的字段,或者用自定义的 MarshalJSON 手动处理。

10.6 时间格式的默认行为

1
2
3
4
t := time.Date(2024, 6, 15, 14, 30, 0, 0, time.Local)
data, _ := json.Marshal(t)
fmt.Println(string(data))
// "2024-06-15T14:30:00+08:00" (RFC3339 格式)

默认输出 RFC3339 格式。如果你的 API 需要其他格式,需要自定义编解码(参见第 9.3 节)。

10.7 解码时多余字段被忽略,缺少字段用零值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

// JSON 中多了 city 字段,会被静默忽略
jsonStr := `{"name":"Alice","age":25,"city":"Beijing"}`
var u User
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {Alice 25}

// JSON 中少了 age 字段,会用零值
jsonStr = `{"name":"Bob"}`
json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u) // {Bob 0}

Go 不会在这些情况下报错。如果需要严格校验,需要自己写验证逻辑。

10.8 json.Number 解决数字精度问题

当数字很大时,float64 会丢失精度:

1
2
3
4
5
6
// 一个很大的整数
jsonStr := `{"id": 9007199254740993}` // 超过 float64 精度

var data map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data)
fmt.Println(data["id"]) // 9007199254740992(丢失精度了!)

json.Number 可以保持原始精度:

1
2
3
4
5
6
7
8
9
10
11
12
13
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.UseNumber()

var data map[string]interface{}
decoder.Decode(&data)

id := data["id"].(json.Number)
fmt.Println(id) // 9007199254740993(精确)
fmt.Println(id.String()) // "9007199254740993"

// 转为 int64
n, _ := id.Int64()
fmt.Println(n) // 9007199254740993

11. 本课练习

练习 1:结构体与 JSON 互转

要求:

  • 定义一个 Student 结构体,包含 IDNameScores(切片)、Grade 字段
  • json:"..." 标签指定 JSON 键名
  • 编码为 JSON 再解码回来,验证数据一致

练习 2:处理 API 响应

要求:

  • 模拟一个 API 返回的 JSON 响应:{"code": 200, "data": [...]}
  • data 是一个用户列表
  • 编写代码解析这个响应并打印每个用户的信息

练习 3:配置文件合并

要求:

  • 读取两个 JSON 配置文件(一个默认配置,一个用户配置)
  • 合并配置:用户配置覆盖默认配置
  • 将合并后的配置写入新文件

练习 4:自定义时间格式

要求:

  • 定义一个 LogEntry 结构体,包含 LevelMessageTime 字段
  • Time 字段编码为 "2006-01-02 15:04:05" 格式(不是默认的 RFC3339)
  • 实现自定义的编解码方法

练习 5:未知 JSON 遍历

要求:

  • 编写一个函数,接收任意 JSON 字符串
  • 统计 JSON 中有多少个对象、多少个数组、多少个叶子节点
  • 输出统计结果

12. 自测题

12.1 概念题

  1. json.Marshaljson.MarshalIndent 有什么区别?
  2. 结构体标签 json:"name,omitempty" 中的 omitempty 是什么意思?
  3. map[string]interface{} 解码 JSON 时,数字会变成什么类型?
  4. json.RawMessage 的用途是什么?
  5. json.NewEncoderjson.Marshal 有什么区别?
  6. 未导出的结构体字段会被 JSON 编解码吗?
  7. json:"-" 标签的作用是什么?
  8. json.Unmarshal 的第二个参数为什么必须传指针?
  9. JSON 中有但 Go 结构体中没有的字段,解码时会怎样?
  10. 如何保持大整数的精度不丢失?

12.2 代码阅读题

预测以下代码的输出:

1
2
3
4
5
6
7
8
9
type Config struct {
Debug bool `json:"debug,omitempty"`
Timeout int `json:"timeout,omitempty"`
Host string `json:"host"`
}

c := Config{Debug: false, Timeout: 0, Host: "localhost"}
data, _ := json.Marshal(c)
fmt.Println(string(data))
点击查看答案
1
{"host":"localhost"}

解释:

  1. Debugfalse,是 bool 的零值,被 omitempty 忽略
  2. Timeout0,是 int 的零值,被 omitempty 忽略
  3. Host 没有 omitempty,所以即使值不为空也会出现
  4. 输出只剩下 host 字段

13. 本课总结

这一课你学到了 Go 中处理 JSON 的完整方法。

场景 推荐方式
Go → JSON json.Marshal / json.MarshalIndent
JSON → Go(已知结构) json.Unmarshal + 结构体
JSON → Go(未知结构) json.Unmarshal + map[string]interface{}
大文件/流式处理 json.NewEncoder / json.NewDecoder
延迟解析 json.RawMessage
自定义格式 实现 Marshaler / Unmarshaler 接口
控制键名 结构体标签 json:"key"
忽略字段 json:"-"
空值忽略 json:"key,omitempty"

最重要的三件事:

  1. 优先用结构体,不要用 map[string]interface{}——类型安全,避免断言陷阱
  2. Unmarshal 必须传指针——这是最常见的错误
  3. omitempty 会忽略 0false——如果这些是有效值,别用 omitempty

14. 下一课预告

下一课我们学习 排序、集合与常见算法工具

会重点讲:

  • sort 包的使用
  • 对切片进行排序
  • 自定义排序规则
  • 常见的集合操作思路

学完下一课,你就能对结构化数据进行基础排序处理了。