news 2026/8/1 17:19:50

json序列化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
json序列化

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.MarshalShouldBindJSON内部调用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分不清零值和未传
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/1 17:19:31

传感器包设计实战:从硬件选型到数据融合的嵌入式开发指南

1. 项目概述:从“传感器包”到智能感知的基石“Sensors Pack”,直译过来就是“传感器包”。乍一听,这名字有点宽泛,甚至有点“偷懒”,好像把一堆传感器打包在一起就完事了。但在我过去十多年接触硬件开发、物联网和嵌入…

作者头像 李华
网站建设 2026/8/1 17:18:54

Linear代码带写服务的技术实现与架构设计

1. Linear代码带写服务解析 最近在开发者社区看到不少关于Linear代码带写的讨论,这种服务模式正在技术圈逐渐流行。作为从业多年的全栈工程师,我想从实际经验出发,系统分析这种服务的技术实现和行业现状。 Linear本质上是一个现代化的项目管…

作者头像 李华
网站建设 2026/8/1 17:13:21

华为性格测试深度解析:求职者如何应对心理测评与价值观匹配

1. 项目概述:为什么“性格测试”成了求职路上的关键一关? 最近几年,无论是应届毕业生还是寻求新机会的职场人,在应聘一些大型科技公司时,都会遇到一个看似与专业技能无关,却又至关重要的环节——性格测试。…

作者头像 李华
网站建设 2026/8/1 17:12:34

PCB材料参数Dk与Df解析:从信号完整性到高速设计选型

1. 项目概述:从“玄学”到“科学”的PCB材料认知在高速数字电路和射频微波领域摸爬滚打十几年,我见过太多工程师在项目后期被信号完整性问题折磨得焦头烂额。眼瞅着原理图逻辑完美,PCB布局布线也规规矩矩,可一上电测试&#xff0c…

作者头像 李华
网站建设 2026/8/1 17:08:41

会调API就能就业?大模型工程师的真正门槛是权限和日志

《AI大模型就业为什么越规划越焦虑?问题可能不在路线》看起来是个大话题,但真落到项目里,常常就是几个具体选择。下面我尽量按实际开发时会遇到的问题来讲。 摘要 最近面试了几个想转大模型的程序员,问他们最近做了什么项目&…

作者头像 李华
网站建设 2026/8/1 17:04:38

Mudra Link在科研中的应用

Mudra Link在科研中的应用 随着可穿戴设备、人工智能和人机交互技术的发展,手势识别已经从简单的图像识别逐渐扩展到肌电信号识别、惯性传感和多模态融合等方向。 Mudra Link是一款面向自然手势交互的智能控制手环,能够通过表面肌电信号和运动传感器获…

作者头像 李华