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:已激活}但数据库里存的还得是1int 查询快、占空间小。1.2 这就是自定义序列化的价值同一个字段在 Go 内存里是int在 JSON 里是string自动双向转换。二、基础序列化 / 反序列化在讲自定义之前先回顾基础用法importencoding/jsontypeUserstruct{IDuintjson:idNamestringjson:name}// 序列化Go 结构体 → JSON 字节data,err:json.Marshal(user)// 反序列化JSON 字节 → Go 结构体err:json.Unmarshal(data,user)结构体 Tag 速查Tag 值作用示例 JSONjson:name指定 JSON key{name: 张三}json:-完全忽略序列化和反序列化都跳过不输出json:name,omitempty零值时省略该字段零值时不输出json:,omitempty使用字段原名零值时省略零值时不输出⚠️ 小写字母开头的字段不会被序列化必须大写开头Go 的导出规则。三、自定义序列化核心模式 ⭐3.1 完整实现步骤以 “用户状态” 为例四步搞定① 定义自定义类型 常量 ② 建正向/反向映射表 ③ 实现 MarshalJSON值接收者→ 序列化int → string ④ 实现 UnmarshalJSON指针接收者→ 反序列化string → int3.2 代码实现packagemainimport(bytesfmtnet/httpgithub.com/gin-gonic/gin)// ① 定义自定义类型typeUserStatusintconst(StatusInactive UserStatusiota// 0StatusActive// 1StatusDisabled// 2)// ② 建映射表正向 反向var(statusToTextmap[UserStatus]string{StatusInactive:未激活,StatusActive:已激活,StatusDisabled:已禁用,}textToStatusmap[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{*sval// 修改指针指向的值returnnil}记忆口诀Marshal 读值Unmarshal 写指针。五、映射表 vs switch两种写法对比写法 1映射表推荐varstatusToTextmap[UserStatus]string{StatusInactive:未激活,StatusActive:已激活,StatusDisabled:已禁用,}vartextToStatusmap[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))}*svalreturnnil}优点新增枚举值只需加一行映射不用改方法逻辑。写法 2switchfunc(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男:*gGenderMalecase女:*gGenderFemaledefault:*gGenderUnknown}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.ReaderGin 的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{IDuintjson:idnamestring// ❌ 小写开头json 完全无视这个字段}// ✅ 必须大写开头typeUserstruct{IDuintjson:idNamestringjson:name}坑 2json:-也忽略反序列化typeUserstruct{Passwordstringjson:-// 序列化时不输出反序列化时也读不进来}// 如果需要只写不读用自定义 MarshalJSON 返回 null 或空坑 3omitempty无法区分未传和传了零值typeReqstruct{Pageintjson:page,omitempty// 前端传 page0 会被省略}// 前端传 {page: 0} → 反序列化后 Page 0// 前端不传 page → 反序列化后 Page 0// 两种情况无法区分// ✅ 用指针typeReqstruct{Page*intjson:page,omitempty// nil 未传0 传了零值}坑 4反序列化必须传指针varuser User json.Unmarshal(data,user)// ❌ 传值类型不会报错但不会赋值json.Unmarshal(data,user)// ✅ 必须传指针坑 5MarshalJSON 返回格式不对func(s UserStatus)MarshalJSON()([]byte,error){return[]byte(已激活),nil// ❌ 缺少双引号不是合法 JSON 字符串return[]byte(已激活),nil// ✅ 带双引号的 JSON 字符串returnjson.Marshal(已激活)// ✅ 用 json.Marshal 更安全}坑 6UnmarshalJSON 忘记去掉双引号func(s*UserStatus)UnmarshalJSON(data[]byte)error{text:string(data)// data 是 已激活带引号直接用会查表失败// ❌ textToStatus[\已激活\] 不存在textstring(bytes.Trim(data,))// ✅ 去掉引号再查表}坑 7时间格式typeEventstruct{Time time.Timejson:time}// 默认输出 RFC3339 格式2024-01-15T10:30:00Z// 如果需要自定义格式实现 MarshalJSONfunc(t MyTime)MarshalJSON()([]byte,error){returnjson.Marshal(time.Time(t).Format(2006-01-02 15:04:05))}九、一句话记忆自定义序列化 自定义类型 映射表 MarshalJSON值接收者读 UnmarshalJSON指针接收者写。数据库里存intJSON 里显示string双向自动转换Marshal 读用值接收者Unmarshal 写用指针接收者映射表比 switch 更好维护新增枚举只改一行json:-连反序列化也忽略omitempty分不清零值和未传