Go JSON 编码:序列化、反序列化与结构体标签
Go JSON 编码:序列化、反序列化与结构体标签
一、为什么需要 JSON 处理
在现代软件开发中,JSON(JavaScript Object Notation)是应用最广泛的数据交换格式。无论是前后端通信、微服务之间的 RPC 调用、还是配置文件存储,JSON 几乎无处不在。Go 语言在标准库中提供了encoding/json包,让 JSON 的序列化和反序列化变得简单而高效。
Go 处理 JSON 的核心思想是:将 JSON 数据映射到 Go 的结构体上。这是因为 Go 是静态类型语言,我们需要在运行时之前就知道数据的形状(shape)。结构体恰好提供了这种"数据蓝图"。
二、序列化:Go 值 → JSON 字节
2.1 基础 Marshal
json.Marshal函数接受任意 Go 值,返回 JSON 格式的字节切片。其函数签名如下:
funcMarshal(vinterface{})([]byte,error)最基础的使用姿势是把一个结构体传进去:
packagemainimport("encoding/json""fmt")typeUserstruct{NamestringAgeintEmailstring}funcmain(){u:=User{Name:"张三",Age:28,Email:"zhangsan@example.com",}data,err:=json.Marshal(u)iferr!=nil{fmt.Println("序列化失败:",err)return}fmt.Println(string(data))// 输出: {"Name":"张三","Age":28,"Email":"zhangsan@example.com"}}注意:默认情况下,JSON 的键名使用的是 Go 结构体的字段名本身(首字母大写),这是 Go 的导出规则决定的——只有导出的字段才能被 JSON 包访问。
2.2 MarshalIndent:可读的 JSON
调试时我们通常希望 JSON 是格式化的。json.MarshalIndent在Marshal基础上增加了缩进控制:
data,err:=json.MarshalIndent(u,""," ")// 第二个参数是每一行的前缀// 第三个参数是每一层级的缩进字符串输出效果:
{"Name":"张三","Age":28,"Email":"zhangsan@example.com"}2.3 支持的类型映射
Go 值与 JSON 类型之间的映射关系:
| Go 类型 | JSON 类型 | 说明 |
|---|---|---|
bool | boolean | 直接映射 |
float32/float64 | number | 所有数字都映射为 float64 |
int/uint系列 | number | 整数也会被 JSON 当作数字 |
string | string | UTF-8 字符串,默认会转义 HTML 字符 |
[]byte | string | Base64 编码的字符串 |
nil/空接口 nil | null | |
slice/array | array | |
map | object | key 必须是 string |
struct | object | 仅导出字段会被包含 |
time.Time | string | RFC 3339 格式 |
三、结构体标签(Struct Tags)
结构体标签是 Go 中一种强大的元编程机制。它是写在结构体字段后面的反引号字符串,为字段提供额外的元数据。JSON 包会根据标签来决定序列化和反序列化时的行为。
3.1 基本语法
typeProductstruct{IDint`json:"id"`Namestring`json:"name"`Pricefloat64`json:"price"`Categorystring`json:"category,omitempty"`internalstring// 小写开头,不会被序列化}标签的格式是json:"键名,选项1,选项2"。主要的选项有:
- omitempty:当字段为零值时不输出该字段
- string:将数字或布尔值序列化成 JSON 字符串(带引号)
- -:完全忽略该字段(即使它是导出的)
3.2 omitempty 的陷阱
omitempty判断"零值"的逻辑和直觉有时不一致:
typeConfigstruct{Portint`json:"port,omitempty"`// 0 时被忽略Timeoutfloat64`json:"timeout,omitempty"`// 0.0 时被忽略Enabledbool`json:"enabled,omitempty"`// false 时被忽略Tags[]string`json:"tags,omitempty"`// nil 或空切片时被忽略// 注意:空结构体{}不会被 omitempty 忽略!}一个常见的坑:当你有一个bool类型的"是否启用"字段,false是合法值,但omitempty会导致它被省略。解决方法是使用指针类型*bool,nil 才表示"未设置"。
3.3 string 选项
string选项会把数字或布尔值序列化为带引号的字符串。这在对接某些要求严格字符串类型的第三方 API 时很有用:
typeResponsestruct{Codeint`json:"code,string"`// 输出 "code": "200" 而不是 "code": 200Okbool`json:"ok,string"`// 输出 "ok": "true" 而不是 "ok": true}四、反序列化:JSON 字节 → Go 值
4.1 基础 Unmarshal
json.Unmarshal将 JSON 字节解析到 Go 值中:
funcUnmarshal(data[]byte,vinterface{})error第二个参数必须是一个指针,因为 Unmarshal 需要修改传入对象的内容:
jsonStr:=`{"name":"李四","age":25,"email":"lisi@example.com"}`typeUserstruct{Namestring`json:"name"`Ageint`json:"age"`Emailstring`json:"email"`}varu User err:=json.Unmarshal([]byte(jsonStr),&u)iferr!=nil{fmt.Println("反序列化失败:",err)return}fmt.Printf("%+v\n",u)// {Name:李四 Age:25 Email:lisi@example.com}4.2 Unmarshal 的容错能力
Go 的 Unmarshal 比较宽容,它遵循以下原则:
- 忽略未知字段:JSON 中有但结构体中没有的字段会被静默跳过。这是一把双刃剑——方便但可能掩盖拼写错误。
- 类型不匹配时静默失败:如果 JSON 中某个字段类型和结构体不匹配,Unmarshal 不会报错,而是返回零值。想严格校验,需要用
json.Decoder的DisallowUnknownFields()。 - 大小写不敏感:如果同时定义了
json:"Name"标签和实际字段名Name,Go 会优先匹配标签名。
4.3 严格反序列化
如果你需要严格的 JSON 反序列化(拒绝未知字段),使用json.Decoder:
funcstrictUnmarshal(data[]byte,vinterface{})error{decoder:=json.NewDecoder(bytes.NewReader(data))decoder.DisallowUnknownFields()returndecoder.Decode(v)}五、嵌套结构与 JSON
现实世界的数据通常是嵌套的。Go 通过嵌套结构体来映射嵌套 JSON:
typeAddressstruct{Citystring`json:"city"`ZipCodestring`json:"zip_code"`}typePersonstruct{Namestring`json:"name"`Ageint`json:"age"`Address Address`json:"address"`Hobbies[]string`json:"hobbies,omitempty"`}funcmain(){p:=Person{Name:"王五",Age:30,Address:Address{City:"北京",ZipCode:"100000",},Hobbies:[]string{"编程","跑步"},}data,_:=json.MarshalIndent(p,""," ")fmt.Println(string(data))}输出:
{"name":"王五","age":30,"address":{"city":"北京","zip_code":"100000"},"hobbies":["编程","跑步"]}5.1 匿名嵌入(组合)
Go 的结构体组合特性在 JSON 中也很好用。当匿名嵌入一个结构体时,其字段会被"提升"到父级 JSON 对象中:
typeBaseModelstruct{IDint`json:"id"`CreatedAtstring`json:"created_at"`}typeArticlestruct{BaseModel// 匿名嵌入,字段会被提升Titlestring`json:"title"`Contentstring`json:"content"`}// 序列化后得到: {"id":1,"created_at":"2026-07-10","title":"Go学习","content":"..."}六、自定义序列化逻辑
6.1 实现 json.Marshaler 和 json.Unmarshaler 接口
当标准行为不满足需求时,可以实现这两个接口来自定义序列化逻辑:
typeMarshalerinterface{MarshalJSON()([]byte,error)}typeUnmarshalerinterface{UnmarshalJSON([]byte)error}一个实际例子:处理灵活的"可能是字符串也可能是数字"的字段:
typeFlexibleIntintfunc(fi*FlexibleInt)UnmarshalJSON(data[]byte)error{// 先尝试按数字解析varnumintiferr:=json.Unmarshal(data,&num);err==nil{*fi=FlexibleInt(num)returnnil}// 再尝试按字符串解析varstrstringiferr:=json.Unmarshal(data,&str);err==nil{n,err:=strconv.Atoi(str)iferr!=nil{returnfmt.Errorf("无法解析为整数: %s",str)}*fi=FlexibleInt(n)returnnil}returnfmt.Errorf("FlexibleInt: 无法解析 %s",string(data))}typeItemstruct{ID FlexibleInt`json:"id"`Namestring`json:"name"`}6.2 json.RawMessage:延迟解析
json.RawMessage本质上是一个[]byte别名,它可以暂存一段原始 JSON 数据,等确定结构后再解析:
typeEventstruct{Typestring`json:"type"`Payload json.RawMessage`json:"payload"`// 先原样保存}// 后续根据 Type 的值决定如何解析 Payloadswitchevent.Type{case"user_login":varlogin LoginPayload json.Unmarshal(event.Payload,&login)case"order_create":varorder OrderPayload json.Unmarshal(event.Payload,&order)}七、流式处理:Encoder 和 Decoder
当处理大量数据或网络流时,使用json.Encoder和json.Decoder比一次性Marshal/Unmarshal更高效——它们直接操作io.Writer/io.Reader,避免了中间大块内存分配。
7.1 Encoder:边写边编码
typeLogEntrystruct{Timestampstring`json:"timestamp"`Levelstring`json:"level"`Messagestring`json:"message"`}// 模拟写入日志文件file,_:=os.Create("app.log")encoder:=json.NewEncoder(file)encoder.SetIndent(""," ")// 可选:格式化输出encoder.Encode(LogEntry{"2026-07-10T12:00:00Z","INFO","服务启动"})encoder.Encode(LogEntry{"2026-07-10T12:01:00Z","ERROR","数据库连接超时"})7.2 Decoder:边读边解码
// 从 HTTP 请求体中流式读取resp,_:=http.Get("https://api.example.com/users")deferresp.Body.Close()decoder:=json.NewDecoder(resp.Body)// 读取开头的 [token,_:=decoder.Token()// 确保是数组开始ifdelim,ok:=token.(json.Delim);!ok||delim!='['{returnerrors.New("期望 JSON 数组")}// 逐个解码数组中的元素fordecoder.More(){varuser Useriferr:=decoder.Decode(&user);err!=nil{break}fmt.Printf("用户: %+v\n",user)}八、实际场景练习
练习 1:配置文件解析器
编写一个从 JSON 配置文件读取数据库连接信息的工具:
packagemainimport("encoding/json""fmt""os")typeDBConfigstruct{Hoststring`json:"host"`Portint`json:"port"`Userstring`json:"user"`Passwordstring`json:"password"`// 实际项目中不应该明文存储Databasestring`json:"database"`Poolstruct{MaxOpenConnsint`json:"max_open_conns"`MaxIdleConnsint`json:"max_idle_conns"`}`json:"pool"`}func(d DBConfig)DSN()string{returnfmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4",d.User,d.Password,d.Host,d.Port,d.Database)}funcLoadDBConfig(pathstring)(*DBConfig,error){data,err:=os.ReadFile(path)iferr!=nil{returnnil,fmt.Errorf("读取配置文件失败: %w",err)}varcfg DBConfig decoder:=json.NewDecoder(bytes.NewReader(data))decoder.DisallowUnknownFields()// 不允许出现未知字段,防止拼写错误iferr:=decoder.Decode(&cfg);err!=nil{returnnil,fmt.Errorf("解析配置文件失败: %w",err)}// 校验必填字段ifcfg.Host==""{returnnil,fmt.Errorf("配置项 host 不能为空")}ifcfg.Port==0{returnnil,fmt.Errorf("配置项 port 不能为空")}return&cfg,nil}练习 2:API 响应包装器
设计一个通用的 API 响应结构,支持任意类型的数据载荷:
packagemainimport("encoding/json""net/http")var(ErrBadRequest=&AppError{Code:400,Message:"请求参数错误"}ErrNotFound=&AppError{Code:404,Message:"资源不存在"}ErrInternal=&AppError{Code:500,Message:"服务器内部错误"})typeAppErrorstruct{Codeint`json:"code"`Messagestring`json:"message"`}func(e*AppError)Error()string{returnfmt.Sprintf("[%d] %s",e.Code,e.Message)}typeAPIResponsestruct{Successbool`json:"success"`Data json.RawMessage`json:"data,omitempty"`Error*AppError`json:"error,omitempty"`}// WriteSuccess 向 http.ResponseWriter 写入成功响应funcWriteSuccess(w http.ResponseWriter,datainterface{})error{dataBytes,err:=json.Marshal(data)iferr!=nil{returnerr}resp:=APIResponse{Success:true,Data:json.RawMessage(dataBytes),}w.Header().Set("Content-Type","application/json; charset=utf-8")w.WriteHeader(http.StatusOK)returnjson.NewEncoder(w).Encode(resp)}// WriteError 向 http.ResponseWriter 写入错误响应funcWriteError(w http.ResponseWriter,errerror){appErr,ok:=err.(*AppError)if!ok{appErr=ErrInternal}resp:=APIResponse{Success:false,Error:appErr,}w.Header().Set("Content-Type","application/json; charset=utf-8")w.WriteHeader(appErr.Code)json.NewEncoder(w).Encode(resp)}九、关键知识点总结
| 知识点 | 核心要点 |
|---|---|
json.Marshal | 将 Go 值序列化为 JSON 字节。只处理导出字段。 |
json.Unmarshal | 将 JSON 字节反序列化为 Go 值。参数必须是指针。默认忽略未知字段和类型不匹配。 |
| 结构体标签 | json:"name,omitempty,string"控制键名、忽略零值、类型包装。 |
omitempty | 零值时跳过字段。注意 bool 的 false 也是零值,可能导致数据丢失。 |
DisallowUnknownFields | 使用 Decoder 设置,拒绝 JSON 中包含结构体未知的字段,适合严格校验的场景。 |
| 自定义序列化 | 实现Marshaler/Unmarshaler接口即可接管单个类型的序列化逻辑。 |
json.RawMessage | 字节别名,可以暂存原始 JSON 数据,延迟到后续确定结构后再解析。 |
| Encoder / Decoder | 流式处理接口,直接操作io.Writer/io.Reader,适合大文件或网络流的场景。 |
十、踩坑提醒
- 未导出的字段不会被序列化:小写开头的字段对 JSON 包不可见。
- omitempty + bool = 陷阱:带
omitempty的 bool 字段,false值会被吞掉。 - 时间格式:
time.Time默认序列化为 RFC 3339 格式(如"2026-07-10T12:00:00Z"),需要其他格式请自定义MarshalJSON。 - map 的 key 必须是 string:JSON 对象的键只能是字符串,所以
map[int]string无法直接序列化——要么换用map[string]string,要么实现自定义序列化。 - []byte 会被 Base64 编码:JSON 不支持原始二进制,
[]byte类型会自动做 Base64 编码。