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

Go JSON 编码:序列化、反序列化与结构体标签

Go JSON 编码:序列化、反序列化与结构体标签
📅 发布时间:2026/7/14 5:29:29

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 类型说明
boolboolean直接映射
float32/float64number所有数字都映射为 float64
int/uint系列number整数也会被 JSON 当作数字
stringstringUTF-8 字符串,默认会转义 HTML 字符
[]bytestringBase64 编码的字符串
nil/空接口 nilnull
slice/arrayarray
mapobjectkey 必须是 string
structobject仅导出字段会被包含
time.TimestringRFC 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,适合大文件或网络流的场景。

十、踩坑提醒

  1. 未导出的字段不会被序列化:小写开头的字段对 JSON 包不可见。
  2. omitempty + bool = 陷阱:带omitempty的 bool 字段,false值会被吞掉。
  3. 时间格式:time.Time默认序列化为 RFC 3339 格式(如"2026-07-10T12:00:00Z"),需要其他格式请自定义MarshalJSON。
  4. map 的 key 必须是 string:JSON 对象的键只能是字符串,所以map[int]string无法直接序列化——要么换用map[string]string,要么实现自定义序列化。
  5. []byte 会被 Base64 编码:JSON 不支持原始二进制,[]byte类型会自动做 Base64 编码。

相关新闻

  • C++图形编程入门:从控制台对角线到图形库绘制的实践指南
  • 一套 0 成本 Vibe Coding 上线流程:从想法到产品 7 步发布上线
  • Apache IoTDB时序数据库可靠测评深度拆解

最新新闻

  • OnscripterYuri WebAssembly优化技巧:提升浏览器游戏性能的10个方法
  • Cocos事件响应混乱的三大解决策略:冒泡、层级与全局管理
  • XMBOX直播功能详解:支持多种直播源协议的播放器
  • 统计学与人工智能:从基础概念到Python实战应用
  • XUnity.AutoTranslator:5分钟掌握Unity游戏自动翻译的终极指南
  • A3910与PIC18LF45K40电机驱动方案解析

日新闻

  • AWS SSM安全运维实践:零公网暴露的合规远程开发方案
  • Tableau 2024.1 图表选择指南:5种业务场景与最佳图表类型匹配
  • dsPIC33FJ与CMT-8540S-SMT在嵌入式音频处理中的高效应用

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

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

服务项目

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

快速链接

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

联系方式

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

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