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 mainimport ( "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)) data, _ = json.Marshal("hello" ) fmt.Println(string (data)) data, _ = json.Marshal(true ) fmt.Println(string (data)) data, _ = json.Marshal([]int {1 , 2 , 3 }) fmt.Println(string (data)) data, _ = json.Marshal(map [string ]int { "apple" : 3 , "banana" : 5 , }) fmt.Println(string (data))
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 键名是 Name、Age,但大多数 API 的 JSON 键名是小写的 name、age。怎么控制输出的键名?用结构体标签。
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:"-"` internal string }
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 }
Password 和 internal 都不会出现在 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"` Debug bool `json:"debug,string"` Timeout int `json:"timeout,omitempty"` }
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 mainimport ( "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) fmt.Println(data["name" ].(string )) fmt.Println(data["age" ].(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) count := data["count" ].(float64 ) fmt.Println(count) fmt.Printf("%T\n" , count) intCount := int (count) fmt.Println(intCount)
如果知道 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 []Usererr := json.Unmarshal([]byte (jsonStr), &users) if err != nil { fmt.Println("解码失败:" , err) return } for _, u := range users { fmt.Println(u) }
5.5 解码时的字段匹配规则
Go 在解码 JSON 到结构体时,匹配规则如下:
精确匹配 JSON 键和结构体标签
大小写不敏感匹配 JSON 键和结构体字段名
忽略 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"` } jsonStr := `{"Name":"Alice","AGE":25,"email":"alice@example.com"}` var u Userjson.Unmarshal([]byte (jsonStr), &u) fmt.Println(u) jsonStr2 := `{"name":"Alice","age":25,"city":"Beijing"}` json.Unmarshal([]byte (jsonStr2), &u) fmt.Println(u)
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 Userjson.Unmarshal([]byte (jsonStr), &u) fmt.Println(u) fmt.Println(u.Address.City)
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 Articlejson.Unmarshal([]byte (jsonStr), &article) fmt.Println(article.Title) fmt.Println(article.Tags[0 ].Name) fmt.Println(article.Tags[1 ].Color)
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" ]) address := data["address" ].(map [string ]interface {}) fmt.Println(address["city" ]) scores := data["scores" ].([]interface {}) fmt.Println(scores[0 ])
问题 :到处都是类型断言,代码很脆弱。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 Responsejson.Unmarshal([]byte (jsonStr), &resp) fmt.Println(resp.Status) fmt.Println(resp.Message) fmt.Println(string (resp.Data)) type UserData struct { Name string `json:"name"` Age int `json:"age"` } var ud UserDatajson.Unmarshal(resp.Data, &ud) fmt.Println(ud)
这种方式在 API 返回多种类型的 data 时非常有用——先看 status 和 message,再决定怎么解析 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 mainimport ( "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 mainimport ( "encoding/json" "fmt" "strings" ) type User struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email"` } func main () { 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 mainimport ( "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"` } 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 } 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 mainimport ( "encoding/json" "fmt" ) type APIResponse struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } 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) 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 () { 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 mainimport ( "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 }, } 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 mainimport ( "encoding/json" "fmt" "time" ) type Duration struct { time.Duration } 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 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"` } e := Event{ Name: "Go 语言分享会" , StartTime: time.Date(2024 , 6 , 15 , 14 , 30 , 0 , 0 , time.Local), } data, _ := json.MarshalIndent(e, "" , " " ) fmt.Println(string (data))
默认的 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))
10. 常见坑总结
10.1 Unmarshal 必须传指针
1 2 3 4 5 6 7 var u Userjson.Unmarshal([]byte (jsonStr), u) var u Userjson.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) price := data["price" ].(int ) price := data["price" ].(float64 ) type Product struct { Price float64 `json:"price"` } var p Productjson.Unmarshal([]byte (jsonStr), &p)
建议 :尽量用结构体而不是 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))
如果 0 或 false 是有效值,不要用 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))
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))
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}
解决办法:不编码导致循环的字段,或者用自定义的 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))
默认输出 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"` } jsonStr := `{"name":"Alice","age":25,"city":"Beijing"}` var u Userjson.Unmarshal([]byte (jsonStr), &u) fmt.Println(u) jsonStr = `{"name":"Bob"}` json.Unmarshal([]byte (jsonStr), &u) fmt.Println(u)
Go 不会在这些情况下报错。如果需要严格校验,需要自己写验证逻辑。
10.8 json.Number 解决数字精度问题
当数字很大时,float64 会丢失精度:
1 2 3 4 5 6 jsonStr := `{"id": 9007199254740993}` var data map [string ]interface {}json.Unmarshal([]byte (jsonStr), &data) fmt.Println(data["id" ])
用 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) fmt.Println(id.String()) n, _ := id.Int64() fmt.Println(n)
11. 本课练习
练习 1:结构体与 JSON 互转
要求:
定义一个 Student 结构体,包含 ID、Name、Scores(切片)、Grade 字段
用 json:"..." 标签指定 JSON 键名
编码为 JSON 再解码回来,验证数据一致
练习 2:处理 API 响应
要求:
模拟一个 API 返回的 JSON 响应:{"code": 200, "data": [...]}
data 是一个用户列表
编写代码解析这个响应并打印每个用户的信息
练习 3:配置文件合并
要求:
读取两个 JSON 配置文件(一个默认配置,一个用户配置)
合并配置:用户配置覆盖默认配置
将合并后的配置写入新文件
练习 4:自定义时间格式
要求:
定义一个 LogEntry 结构体,包含 Level、Message、Time 字段
Time 字段编码为 "2006-01-02 15:04:05" 格式(不是默认的 RFC3339)
实现自定义的编解码方法
练习 5:未知 JSON 遍历
要求:
编写一个函数,接收任意 JSON 字符串
统计 JSON 中有多少个对象、多少个数组、多少个叶子节点
输出统计结果
12. 自测题
12.1 概念题
json.Marshal 和 json.MarshalIndent 有什么区别?
结构体标签 json:"name,omitempty" 中的 omitempty 是什么意思?
用 map[string]interface{} 解码 JSON 时,数字会变成什么类型?
json.RawMessage 的用途是什么?
json.NewEncoder 和 json.Marshal 有什么区别?
未导出的结构体字段会被 JSON 编解码吗?
json:"-" 标签的作用是什么?
json.Unmarshal 的第二个参数为什么必须传指针?
JSON 中有但 Go 结构体中没有的字段,解码时会怎样?
如何保持大整数的精度不丢失?
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))
点击查看答案
解释:
Debug 是 false,是 bool 的零值,被 omitempty 忽略
Timeout 是 0,是 int 的零值,被 omitempty 忽略
Host 没有 omitempty,所以即使值不为空也会出现
输出只剩下 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"
最重要的三件事:
优先用结构体,不要用 map[string]interface{}——类型安全,避免断言陷阱
Unmarshal 必须传指针——这是最常见的错误
omitempty 会忽略 0 和 false——如果这些是有效值,别用 omitempty
14. 下一课预告
下一课我们学习 排序、集合与常见算法工具 。
会重点讲:
sort 包的使用
对切片进行排序
自定义排序规则
常见的集合操作思路
学完下一课,你就能对结构化数据进行基础排序处理了。