尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

json序列化

json序列化
📅 发布时间:2026/8/1 17:40:43

Go 的 JSON 序列化到底怎么玩?为什么前后端状态字段总对不上?

一句话总结

Go 的encoding/json包通过结构体 Tag 和自定义MarshalJSON/UnmarshalJSON,让数据库里的int在 JSON 里变成人类可读的文本,而且双向自动转换。

它解决的核心问题是:

「数据库存的是 0/1/2,前端想要的是 “未激活”/“已激活”/“已禁用”,怎么让两边都满意?」


一、为什么需要自定义序列化?

1.1 一个真实场景

你有一个用户表,状态字段用int存储:

值含义
0未激活
1已激活
2已禁用

如果直接json.Marshal,前端收到的是:

{"id":101,"name":"张三","status":1}

前端同学的灵魂拷问:“1 是什么意思?我还要维护一份映射表?”

你想要的是:

{"id":101,"name":"张三","status":"已激活"}

但数据库里存的还得是1(int 查询快、占空间小)。

1.2 这就是自定义序列化的价值

同一个字段,在 Go 内存里是int,在 JSON 里是string,自动双向转换。


二、基础序列化 / 反序列化

在讲自定义之前,先回顾基础用法:

import"encoding/json"typeUserstruct{IDuint`json:"id"`Namestring`json:"name"`}// 序列化:Go 结构体 → JSON 字节data,err:=json.Marshal(user)// 反序列化:JSON 字节 → Go 结构体err:=json.Unmarshal(data,&user)

结构体 Tag 速查

Tag 值作用示例 JSON
json:"name"指定 JSON key{"name": "张三"}
json:"-"完全忽略(序列化和反序列化都跳过)不输出
json:"name,omitempty"零值时省略该字段零值时不输出
json:",omitempty"使用字段原名,零值时省略零值时不输出

⚠️ 小写字母开头的字段不会被序列化,必须大写开头(Go 的导出规则)。


三、自定义序列化:核心模式 ⭐

3.1 完整实现步骤

以 “用户状态” 为例,四步搞定:

① 定义自定义类型 + 常量 ② 建正向/反向映射表 ③ 实现 MarshalJSON(值接收者)→ 序列化:int → string ④ 实现 UnmarshalJSON(指针接收者)→ 反序列化:string → int

3.2 代码实现

packagemainimport("bytes""fmt""net/http""github.com/gin-gonic/gin")// ① 定义自定义类型typeUserStatusintconst(StatusInactive UserStatus=iota// 0StatusActive// 1StatusDisabled// 2)// ② 建映射表:正向 + 反向var(statusToText=map[UserStatus]string{StatusInactive:"未激活",StatusActive:"已激活",StatusDisabled:"已禁用",}textToStatus=map[string]UserStatus{"未激活":StatusInactive,"已激活":StatusActive,"已禁用":StatusDisabled,})//MarshalJSON:数字 → 文本(值接收者)func(s UserStatus)MarshalJSON()([]byte,error){text,exists:=statusToText[s]if!exists{text="未知状态"}return[]byte(fmt.Sprintf(`"%s"`, text)), nil } //UnmarshalJSON:文本 → 数字(指针接收者) func (s *UserStatus) UnmarshalJSON(data []byte) error { trimmedData := bytes.Trim(data, `"`) text := string(trimmedData) val, exists := textToStatus[text] if !exists { return fmt.Errorf("无效的状态文本: %s", text) } *s = val return nil } // 业务结构体 type User struct { ID uint `json:"id"` Name string `json:"name"` Status UserStatus `json:"status"\`}

3.3 使用效果

// 序列化:Go → JSONuser:=User{ID:101,Name:"张三",Status:StatusActive}data,_:=json.Marshal(user)// 输出:{"id":101,"name":"张三","status":"已激活"}// 反序列化:JSON → Govaru User json.Unmarshal([]byte(`{"id":101,"name":"张三","status":"已禁用"}`),&u)// u.Status == 2 (StatusDisabled)

3.4 Gin 中的实际使用

r.GET("/user",func(c*gin.Context){user:=User{ID:101,Name:"张三",Status:StatusActive}c.JSON(http.StatusOK,user)// 自动输出: {"status":"已激活"}})r.POST("/user",func(c*gin.Context){varuser Useriferr:=c.ShouldBindJSON(&user);err!=nil{c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})return}fmt.Printf("解析出的状态数字为: %d\n",user.Status)// 1c.JSON(http.StatusOK,gin.H{"message":"接收成功"})})

Gin 的c.JSON()内部调用json.Marshal,ShouldBindJSON内部调用json.Unmarshal,所以自定义方法自动生效。


四、为什么接收者类型不同?

这是最容易搞混的点:

方法接收者为什么
MarshalJSON值接收者(s UserStatus)序列化是"读取",不需要修改自身
UnmarshalJSON指针接收者(s *UserStatus)反序列化是"写入",必须修改自身
// 值接收者:读 s 的值,转成 JSONfunc(s UserStatus)MarshalJSON()([]byte,error){...}// 指针接收者:把 JSON 解析后的值写回 sfunc(s*UserStatus)UnmarshalJSON(data[]byte)error{*s=val// 修改指针指向的值returnnil}

记忆口诀:Marshal 读(值),Unmarshal 写(指针)。


五、映射表 vs switch:两种写法对比

写法 1:映射表(推荐)

varstatusToText=map[UserStatus]string{StatusInactive:"未激活",StatusActive:"已激活",StatusDisabled:"已禁用",}vartextToStatus=map[string]UserStatus{"未激活":StatusInactive,"已激活":StatusActive,"已禁用":StatusDisabled,}func(s UserStatus)MarshalJSON()([]byte,error){text,ok:=statusToText[s]if!ok{text="未知状态"}return[]byte(fmt.Sprintf(`"%s"`, text)), nil } func (s *UserStatus) UnmarshalJSON(data []byte) error { trimmedData := bytes.Trim(data, `"`)val,ok:=textToStatus[string(trimmedData)]if!ok{returnfmt.Errorf("无效的状态文本: %s",string(trimmedData))}*s=valreturnnil}

优点:新增枚举值只需加一行映射,不用改方法逻辑。

写法 2:switch

func(g Gender)MarshalJSON()([]byte,error){returnjson.Marshal(g.String())}func(g Gender)String()string{switchg{caseGenderMale:return"男"caseGenderFemale:return"女"default:return"未知"}}func(g*Gender)UnmarshalJSON(data[]byte)error{varsstringiferr:=json.Unmarshal(data,&s);err!=nil{returnerr}switchs{case"男":*g=GenderMalecase"女":*g=GenderFemaledefault:*g=GenderUnknown}returnnil}

优点:不需要额外的映射表变量;缺点:每加一个值要改两处 switch。

枚举值少(2-3 个)用 switch 也行,多了建议映射表。


六、流式读写(Encoder / Decoder)

当处理 HTTP 请求/响应、文件等io.Reader/io.Writer时,用流式更高效:

// 写入:Encoderjson.NewEncoder(w).Encode(data)// w 实现 io.Writer(如 http.ResponseWriter)// 读取:Decodervaruser User json.NewDecoder(r.Body).Decode(&user)// r.Body 实现 io.Reader

Gin 的c.JSON()内部就是用json.NewEncoder流式写入。


七、map 和切片的序列化

// mapm:=map[string]int{"a":1,"b":2}data,_:=json.Marshal(m)// {"a":1,"b":2}// 切片nums:=[]int{1,2,3}data,_:=json.Marshal(nums)// [1,2,3]// 反序列化到 mapvarresultmap[string]intjson.Unmarshal(data,&result)// 反序列化到 interface{}(动态结构)varanyinterface{}json.Unmarshal(data,&any)

八、常见陷阱

坑 1:结构体字段未导出

typeUserstruct{IDuint`json:"id"`namestring// ❌ 小写开头,json 完全无视这个字段}// ✅ 必须大写开头typeUserstruct{IDuint`json:"id"`Namestring`json:"name"`}

坑 2:json:"-"也忽略反序列化

typeUserstruct{Passwordstring`json:"-"`// 序列化时不输出,反序列化时也读不进来}// 如果需要"只写不读":用自定义 MarshalJSON 返回 null 或空

坑 3:omitempty无法区分"未传"和"传了零值"

typeReqstruct{Pageint`json:"page,omitempty"`// 前端传 page=0 会被省略}// 前端传 {"page": 0} → 反序列化后 Page == 0// 前端不传 page → 反序列化后 Page == 0// 两种情况无法区分!// ✅ 用指针typeReqstruct{Page*int`json:"page,omitempty"`// nil = 未传,0 = 传了零值}

坑 4:反序列化必须传指针

varuser User json.Unmarshal(data,user)// ❌ 传值类型,不会报错但不会赋值json.Unmarshal(data,&user)// ✅ 必须传指针

坑 5:MarshalJSON 返回格式不对

func(s UserStatus)MarshalJSON()([]byte,error){return[]byte("已激活"),nil// ❌ 缺少双引号,不是合法 JSON 字符串return[]byte(`"已激活"`),nil// ✅ 带双引号的 JSON 字符串returnjson.Marshal("已激活")// ✅ 用 json.Marshal 更安全}

坑 6:UnmarshalJSON 忘记去掉双引号

func(s*UserStatus)UnmarshalJSON(data[]byte)error{text:=string(data)// data 是 `"已激活"`(带引号),直接用会查表失败// ❌ textToStatus["\"已激活\""] 不存在text=string(bytes.Trim(data,`"`))// ✅ 去掉引号再查表}

坑 7:时间格式

typeEventstruct{Time time.Time`json:"time"`}// 默认输出 RFC3339 格式:2024-01-15T10:30:00Z// 如果需要自定义格式,实现 MarshalJSON:func(t MyTime)MarshalJSON()([]byte,error){returnjson.Marshal(time.Time(t).Format("2006-01-02 15:04:05"))}

九、一句话记忆

自定义序列化 = 自定义类型 + 映射表 + MarshalJSON(值接收者读)+ UnmarshalJSON(指针接收者写)。

  • 数据库里存int,JSON 里显示string,双向自动转换
  • Marshal 读用值接收者,Unmarshal 写用指针接收者
  • 映射表比 switch 更好维护,新增枚举只改一行
  • json:"-"连反序列化也忽略,omitempty分不清零值和未传

相关新闻

  • 树莓派SPI驱动3.2寸LCD屏全攻略:从硬件连接到LVGL图形界面
  • 【无人机三维路径规划】基于灰狼算法GWO实现复杂城市地形无人机避障三维航迹规划附Matlab代码
  • Speechless微博备份完整指南:3分钟学会永久保存社交记忆

最新新闻

  • FF14国际服汉化终极指南:3分钟解锁完整中文游戏体验
  • LuckyLilliaBot:5分钟快速搭建全能QQ机器人框架的终极指南
  • WPF中高效集成SVG图标:SharpVectors方案详解与实战优化
  • Unity游戏数据存储方案全解析:从PlayerPrefs到健壮存档系统设计
  • 潍坊漏水检测设备实测:知途管道科技技术团队6大主流设备横评与选型指南 - 知途管道科技
  • ArcGIS地图包创建全攻略:从原理到实践,解决数据分享难题

日新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号