28 - Go JSON 数据操作

文章目录


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。

相关推荐
三*一1 小时前
Mapbox GL JS 自研面要素整形工具开发实录
开发语言·javascript·arcgis·ecmascript
超级小星星2 小时前
C 语言结构体内存对齐深度解析:从概念到实战
c语言·开发语言
狮子座明仔2 小时前
AgentSPEX:当 Agent 框架开始把“控制流“从 Python 里抠出来
开发语言·python
笨笨饿2 小时前
74_SysTick滴答定时器中断
c语言·开发语言·人工智能·单片机·嵌入式硬件·算法·学习方法
科芯创展2 小时前
XZ4058B/C,20V,外置MOS,8.4V/8.7V开关充电芯片 宽范围电源电压:8.9V~20V-(电池充电电压:8.4V/8.7V)
c语言·开发语言
AI玫瑰助手3 小时前
Python流程控制:break与continue语句的区别与应用
开发语言·python·信息可视化
largecode3 小时前
如何让电话显示店名?来电显示店铺名称,提升有效接通率
java·开发语言·spring·百度·学习方法·业界资讯·twitter
xuhaoyu_cpp_java3 小时前
SpringMVC学习(五)
java·开发语言·经验分享·笔记·学习·spring