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 → int3.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.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{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分不清零值和未传
编程学习
技术分享
实战经验