文章目录
- [28 - Go JSON 数据操作(重点🔥)](#28 - Go JSON 数据操作(重点🔥))
- 核心概念
-
- [JSON 解决了什么问题?](#JSON 解决了什么问题?)
- [Go 中 JSON 的本质是什么?](#Go 中 JSON 的本质是什么?)
- [为什么 Go 用反射做 JSON?](#为什么 Go 用反射做 JSON?)
- 小结
- 基础使用示例
- [JSON Tag(重点🔥)](#JSON Tag(重点🔥))
- 进阶使用示例
-
- [使用 map[string]interface{}](#使用 map[string]interface{})
- [注意坑:数字默认 float64](#注意坑:数字默认 float64)
- 小结
- 嵌套结构体解析
- [自定义 JSON 序列化(高级)](#自定义 JSON 序列化(高级))
-
- [实现 Marshaler 接口](#实现 Marshaler 接口)
- [为什么需要 Alias?](#为什么需要 Alias?)
- 常见错误与坑(重点🔥)
- 坑一:未导出字段无法解析
- 底层原理解析(核心🔥)
- [encoding/json 的工作流程](#encoding/json 的工作流程)
-
- [Marshal 流程](#Marshal 流程)
- [Unmarshal 流程](#Unmarshal 流程)
- 为什么性能不高?
- 为什么标准库仍然使用反射?
- [JSON Decoder(重要🔥)](#JSON Decoder(重要🔥))
- 对比与扩展
- [json.Marshal vs json.NewEncoder](#json.Marshal vs json.NewEncoder)
- [struct vs map[string]interface{}](#struct vs map[string]interface{})
- [标准库 vs 第三方 JSON 库](#标准库 vs 第三方 JSON 库)
- 最佳实践(非常重要🔥)
- [优先使用 struct](#优先使用 struct)
- [API 对象与数据库对象分离](#API 对象与数据库对象分离)
- [谨慎使用 omitempty](#谨慎使用 omitempty)
- [大 JSON 使用 Decoder](#大 JSON 使用 Decoder)
- 自定义时间解析
- 不要忽略错误
- 思考与升华(加分项🔥)
- [如果让你自己实现 json.Unmarshal?](#如果让你自己实现 json.Unmarshal?)
- 更深层的本质
- 最后的总结
28 - Go JSON 数据操作(重点🔥)
在现代后端开发里,JSON 几乎无处不在:
- HTTP API
- 配置文件
- Kafka 消息
- RPC 数据交换
- WebSocket
- 数据库存储
而 Go 之所以在云原生、微服务领域如此流行,一个非常重要的原因就是:
Go 标准库自带了极其强大的 JSON 支持。
但很多人只会:
go
json.Marshal() // 序列化
json.Unmarshal() // 反序列化
真正到了生产环境:
- 字段丢失
- 精度错误
- tag 混乱
- interface{} 地狱
- 动态 JSON
- 性能问题
- 零值陷阱
- 数字类型坑
就开始爆炸。
这篇文章,我们不只是讲"怎么用"。
更重要的是:
Go 为什么这样设计 JSON?
encoding/json 的本质是什么?
真实项目里到底应该怎么写?
核心概念
JSON 解决了什么问题?
JSON(JavaScript Object Notation)本质上是:
一种跨语言的数据交换格式。
它解决的是:
- 不同系统之间的数据传输
- 不同语言之间的数据兼容
- 人类可读的数据表达
例如:
json
{
"name": "zhangsan",
"age": 18
}
Python 能读。
Java 能读。
Go 也能读。
Go 中 JSON 的本质是什么?
Go 的 JSON 本质是:
"结构体" 与 "文本数据" 的相互映射。
核心流程:
text
Go Struct (静态)
↓
反射(reflect) → 字段信息
↓
JSON Encoder → 文本数据
↓
JSON 字符串 (动态)
反过来也一样:
text
JSON 字符串 (动态)
↓
JSON Decoder → 字段信息
↓
反射设置字段 (静态)
↓
Go Struct (静态)
也就是说:
encoding/json 本质是 "基于反射的结构体序列化框架"。
为什么 Go 用反射做 JSON?
因为:
JSON 字段是动态的。
而 Go 类型是静态的。
所以必须:
- 在运行时获取字段信息
- 获取 tag
- 获取字段类型
- 动态赋值
这只能依赖 reflect。
小结
JSON 在 Go 中不是"字符串处理"。
而是:
"运行时类型系统 + 反射" 的数据映射过程。
理解这一点,后边很多坑你都会瞬间明白。
基础使用示例
最简单的 JSON 编码与解码
go
package main
import (
"encoding/json"
"fmt"
)
// 定义一个结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// Go 对象
user := User{ // 结构体初始化
Name: "张三", // 字段初始化
Age: 18, // 字段初始化
}
// struct -> json
jsonBytes, err := json.Marshal(user) // 序列化
if err != nil { // 序列化失败处理
panic(err)
}
fmt.Println(string(jsonBytes)) // 打印序列化后的json字符串
// json -> struct
var newUser User
err = json.Unmarshal(jsonBytes, &newUser)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", newUser)
}
输出:
json
{"name":"张三","age":18}
{Name:张三 Age:18}
核心点解析
Marshal
作用:
go
struct -> json
本质:
- 反射读取结构体字段
- 读取 json tag
- 拼接 JSON 字符串
Unmarshal
作用:
go
json -> struct
本质:
- 解析 JSON Token
- 根据字段名匹配 struct
- 通过反射赋值
为什么 Unmarshal 必须传指针?
错误写法:
go
json.Unmarshal(data, user)
正确写法:
go
json.Unmarshal(data, &user)
原因:
因为需要修改对象。
Go 参数传递是值拷贝。
不传指针:
- 修改的是副本
- 原对象不会变化
小结
JSON 操作其实就两件事:
text
编码(Marshal)
解码(Unmarshal)
但真正复杂的是:
"类型映射"。
JSON Tag(重点🔥)
json tag 的作用
go
type User struct {
Name string `json:"name"`
}
作用:
- 指定 JSON 字段名
- 控制序列化行为
- 忽略字段
- omitempty
字段改名
go
type User struct {
UserName string `json:"user_name"`
}
输出:
json
{
"user_name": "zhangsan"
}
忽略字段
go
package main
import (
"fmt"
)
// 定义一个结构体
type User struct {
Password string `json:"-"`
}
func main() {
user := User{Password: "123456"}
fmt.Println(user)
fmt.Println(string(user.Password))
}
输出:
text
{123456}
123456
不会输出。
适用于:
- 密码
- token
- 内部字段
omitempty
go
package main
import (
"fmt"
)
// 定义一个结构体
type User struct {
Name string `json:"name,omitempty"`
}
func main() {
user := User{}
fmt.Println(user)
}
零值时不输出。
例如:
go
User{}
输出:
json
{}
注意:omitempty 的坑
go
type User struct {
Age int `json:"age,omitempty"`
}
当:
go
Age = 0
字段会消失。
但:
0 有时候是合法值!
正确方案(指针)
go
package main
import (
"fmt"
)
// 定义一个结构体
type User struct {
Age *int `json:"age,omitempty"`
}
func main() {
age := 0
user := User{Age: &age}
fmt.Println(*user.Age) // 这里会输出 "0"
}
这样:
go
0 != nil
就能区分:
- 未传
- 值为 0
进阶使用示例
使用 map[string]interface{}
go
package main
import (
"encoding/json"
"fmt"
)
func main() {
// 解析json字符串到map[string]interface{}
jsonStr := `{
"name":"zhangsan",
"age":18
}`
// 定义一个map[string]interface{}类型的变量来接收解析后的结果
var result map[string]interface{}
// 解析json字符串到map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &result)
if err != nil {
panic(err)
}
fmt.Println(result["name"])
fmt.Println(result["age"])
}
注意坑:数字默认 float64
输出:
go
zhangsan
18
但真实类型:
go
float64
因为:
JSON 没有 int/float 区分。
encoding/json 默认:
go
number -> float64
小结
动态 JSON 很灵活。
但代价是:
失去类型安全。
这也是 Go 官方更推荐 struct 的原因。
嵌套结构体解析
JSON
json
{
"name":"zhangsan",
"address":{
"city":"beijing"
}
}
Go
go
package main
import (
"encoding/json"
"fmt"
)
// Address 结构体,用于表示地址信息
type Address struct {
City string `json:"city"`
}
// User 结构体,包含一个 Address 类型字段
type User struct {
Name string `json:"name"`
Address Address `json:"address"` // 嵌套结构体字段
}
func main() {
jsonStr := `{
"name":"zhangsan",
"address":{
"city":"beijing"
}
}`
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", user)
}
输出:
text
{Name:zhangsan Address:{City:beijing}}
自定义 JSON 序列化(高级)
很多时候:
默认 JSON 不够用。
比如:
- 时间格式化
- 脱敏
- 特殊字段
- 自定义协议
实现 Marshaler 接口
go
package main
import (
"encoding/json"
"fmt"
)
// User 自定义结构体
type User struct {
Name string
}
// MarshalJSON 自定义 JSON 序列化方法
func (u User) MarshalJSON() ([]byte, error) {
// 匿名字段,相当于嵌入 User 结构体
type Alias User
return json.Marshal(&struct {
Name string `json:"name"` // 自定义字段名
Alias // 嵌入 User
}{
Name: "***" + u.Name + "***", // 修改字段值
Alias: (Alias)(u), // 嵌入 User
})
}
func main() {
// 创建 User 实例
user := User{
Name: "zhangsan",
}
// 序列化 User 为 JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
}
输出:
json
{"name":"***zhangsan***","Name":"zhangsan"}
为什么需要 Alias?
否则:
go
json.Marshal(u)
会递归调用自己。
导致:
text
栈溢出
这是生产里非常经典的坑。
常见错误与坑(重点🔥)
坑一:未导出字段无法解析
错误代码
go
type User struct {
name string `json:"name"`
}
go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
name string `json:"name"`
}
func main() {
user := User{name: "Alice"}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}
fmt.Println(string(jsonData))
}
输出:
text
{}
为什么会错?
Go 反射只能访问:
text
导出字段(大写)
小写字段:
- 不可见
- reflect 无权限
因此:
- Marshal 不会输出
- Unmarshal 不会赋值
正确写法
go
type User struct {
Name string `json:"name"`
}
小结
JSON 基于反射。
而反射遵守 Go 可见性规则。
底层原理解析(核心🔥)
encoding/json 的工作流程
Marshal 流程
大致流程:
text
Marshal
↓
reflect.ValueOf
↓
遍历结构体字段
↓
读取 tag
↓
字段编码
↓
拼接 JSON
Unmarshal 流程
text
JSON 字符串
↓
Lexer 词法解析
↓
Token 流
↓
反射创建对象
↓
字段匹配
↓
类型转换
↓
赋值
为什么性能不高?
因为:
反射非常重。
包括:
- 类型检查
- 动态调用
- 内存分配
- map 查找
所以:
标准库 JSON 并不是最快的。
为什么标准库仍然使用反射?
因为:
通用性优先。
Go 官方更在意:
- 稳定
- 易用
- 兼容性
而不是极致性能。
JSON Decoder(重要🔥)
避免一次性加载大 JSON
很多人:
go
json.Unmarshal(bigData)
如果:
- 100MB
- 1GB
直接内存爆炸。
正确方式:流式解析
go
package main
import (
"encoding/json"
"fmt"
"strings"
)
// 定义一个结构体
type User struct {
Name string `json:"name"`
}
func main() {
// 定义一个json字符串, 其中包含多个json对象
jsonStream := `
{"name":"a"}
{"name":"b"}
{"name":"c"}
`
// 创建一个json解码器, 以字符串的形式读取json数据
decoder := json.NewDecoder(strings.NewReader(jsonStream))
// 循环解码json数据, 直到没有更多的数据可供解码
for {
var user User
// 解码json数据, 如果出错则退出循环
err := decoder.Decode(&user) // 将json数据解码到user结构体中
if err != nil {
break
}
fmt.Println(user.Name)
}
}
输出:
text
a
b
c
小结
Decoder 的本质:
流式 Token 解析器。
适用于:
- 大文件
- 网络流
- Kafka
- WebSocket
对比与扩展
json.Marshal vs json.NewEncoder
Marshal
特点:
- 一次性生成 []byte
- 小数据方便
适合:
- HTTP 返回
- 普通对象
Encoder
特点:
- 流式写入
- 更节省内存
适合:
- 文件
- socket
- 大 JSON
struct vs map[string]interface{}
struct
优点:
- 类型安全
- 性能更高
- IDE 友好
缺点:
- 不够灵活
适合:
- 固定协议
- API
- 配置
map[string]interface{}
优点:
- 灵活
缺点:
- 类型断言复杂
- 容易 panic
- 性能更差
适合:
- 动态字段
- 未知 JSON
标准库 vs 第三方 JSON 库
encoding/json
优点:
- 官方标准
- 稳定
- 兼容性最好
缺点:
- 性能一般
jsoniter
特点:
- 更快
- API 兼容标准库
适合:
- 高性能场景
sonic
特点:
- 极致性能
- 字节跳动出品
适合:
- 超高 QPS
点睛总结
JSON 的核心矛盾:
"动态数据" 与 "静态类型" 的冲突。
而 Go 的 encoding/json:
本质是在两者之间做平衡。
最佳实践(非常重要🔥)
优先使用 struct
不要滥用:
go
map[string]interface{}
真实项目里:
90% 应该用 struct。
API 对象与数据库对象分离
不要:
go
一个 struct 走天下
建议:
text
DTO
VO
DO
Entity
分层。
否则:
- tag 混乱
- 字段污染
- 维护困难
谨慎使用 omitempty
尤其:
- bool
- int
- float
零值不一定代表"空"。
大 JSON 使用 Decoder
不要:
go
ioutil.ReadAll()
尤其:
- 文件
- 网络流
自定义时间解析
生产环境里:
时间格式经常不统一。
建议:
统一封装时间类型。
不要忽略错误
很多人:
go
data, _ := json.Marshal(v)
这是事故高发点。
思考与升华(加分项🔥)
如果让你自己实现 json.Unmarshal?
核心步骤其实是:
text
读取字符
↓
词法解析
↓
生成 Token
↓
识别对象结构
↓
反射创建 Go 对象
↓
字段匹配
↓
类型转换
↓
赋值
你会发现:
JSON 框架本质是"解释器"。
更深层的本质
Go JSON 设计,其实体现了 Go 的哲学:
- 明确优于隐式
- 类型安全优于动态魔法
- 工程稳定优于炫技
因此:
Go 不会像 Python 那样:
python
json.xxx
无限动态。
而是强调:
go
struct + tag + 显式定义
因为:
可维护性,永远比"省几行代码"更重要。
最后的总结
真正掌握 Go JSON,不是会:
go
Marshal / Unmarshal
而是理解:
- 反射如何工作
- 类型如何映射
- 为什么会有 float64 坑
- 为什么 omitempty 会丢字段
- 为什么 struct 才是工程核心
- 为什么 Decoder 更适合大数据流
当你开始从:
"JSON 是字符串"
转变为:
"JSON 是运行时类型映射"
你才算真正理解了 Go JSON。