Go JSON 编码序列化、反序列化与结构体标签一、为什么需要 JSON 处理在现代软件开发中JSONJavaScript Object Notation是应用最广泛的数据交换格式。无论是前后端通信、微服务之间的 RPC 调用、还是配置文件存储JSON 几乎无处不在。Go 语言在标准库中提供了encoding/json包让 JSON 的序列化和反序列化变得简单而高效。Go 处理 JSON 的核心思想是将 JSON 数据映射到 Go 的结构体上。这是因为 Go 是静态类型语言我们需要在运行时之前就知道数据的形状shape。结构体恰好提供了这种数据蓝图。二、序列化Go 值 → JSON 字节2.1 基础 Marshaljson.Marshal函数接受任意 Go 值返回 JSON 格式的字节切片。其函数签名如下funcMarshal(vinterface{})([]byte,error)最基础的使用姿势是把一个结构体传进去packagemainimport(encoding/jsonfmt)typeUserstruct{NamestringAgeintEmailstring}funcmain(){u:User{Name:张三,Age:28,Email:zhangsanexample.com,}data,err:json.Marshal(u)iferr!nil{fmt.Println(序列化失败:,err)return}fmt.Println(string(data))// 输出: {Name:张三,Age:28,Email:zhangsanexample.com}}注意默认情况下JSON 的键名使用的是 Go 结构体的字段名本身首字母大写这是 Go 的导出规则决定的——只有导出的字段才能被 JSON 包访问。2.2 MarshalIndent可读的 JSON调试时我们通常希望 JSON 是格式化的。json.MarshalIndent在Marshal基础上增加了缩进控制data,err:json.MarshalIndent(u,, )// 第二个参数是每一行的前缀// 第三个参数是每一层级的缩进字符串输出效果{Name:张三,Age:28,Email:zhangsanexample.com}2.3 支持的类型映射Go 值与 JSON 类型之间的映射关系Go 类型JSON 类型说明boolboolean直接映射float32/float64number所有数字都映射为 float64int/uint系列number整数也会被 JSON 当作数字stringstringUTF-8 字符串默认会转义 HTML 字符[]bytestringBase64 编码的字符串nil/空接口 nilnullslice/arrayarraymapobjectkey 必须是 stringstructobject仅导出字段会被包含time.TimestringRFC 3339 格式三、结构体标签Struct Tags结构体标签是 Go 中一种强大的元编程机制。它是写在结构体字段后面的反引号字符串为字段提供额外的元数据。JSON 包会根据标签来决定序列化和反序列化时的行为。3.1 基本语法typeProductstruct{IDintjson:idNamestringjson:namePricefloat64json:priceCategorystringjson:category,omitemptyinternalstring// 小写开头不会被序列化}标签的格式是json:键名,选项1,选项2。主要的选项有omitempty当字段为零值时不输出该字段string将数字或布尔值序列化成 JSON 字符串带引号-完全忽略该字段即使它是导出的3.2 omitempty 的陷阱omitempty判断零值的逻辑和直觉有时不一致typeConfigstruct{Portintjson:port,omitempty// 0 时被忽略Timeoutfloat64json:timeout,omitempty// 0.0 时被忽略Enabledbooljson:enabled,omitempty// false 时被忽略Tags[]stringjson:tags,omitempty// nil 或空切片时被忽略// 注意空结构体{}不会被 omitempty 忽略}一个常见的坑当你有一个bool类型的是否启用字段false是合法值但omitempty会导致它被省略。解决方法是使用指针类型*boolnil 才表示未设置。3.3 string 选项string选项会把数字或布尔值序列化为带引号的字符串。这在对接某些要求严格字符串类型的第三方 API 时很有用typeResponsestruct{Codeintjson:code,string// 输出 code: 200 而不是 code: 200Okbooljson:ok,string// 输出 ok: true 而不是 ok: true}四、反序列化JSON 字节 → Go 值4.1 基础 Unmarshaljson.Unmarshal将 JSON 字节解析到 Go 值中funcUnmarshal(data[]byte,vinterface{})error第二个参数必须是一个指针因为 Unmarshal 需要修改传入对象的内容jsonStr:{name:李四,age:25,email:lisiexample.com}typeUserstruct{Namestringjson:nameAgeintjson:ageEmailstringjson:email}varu User err:json.Unmarshal([]byte(jsonStr),u)iferr!nil{fmt.Println(反序列化失败:,err)return}fmt.Printf(%v\n,u)// {Name:李四 Age:25 Email:lisiexample.com}4.2 Unmarshal 的容错能力Go 的 Unmarshal 比较宽容它遵循以下原则忽略未知字段JSON 中有但结构体中没有的字段会被静默跳过。这是一把双刃剑——方便但可能掩盖拼写错误。类型不匹配时静默失败如果 JSON 中某个字段类型和结构体不匹配Unmarshal 不会报错而是返回零值。想严格校验需要用json.Decoder的DisallowUnknownFields()。大小写不敏感如果同时定义了json:Name标签和实际字段名NameGo 会优先匹配标签名。4.3 严格反序列化如果你需要严格的 JSON 反序列化拒绝未知字段使用json.DecoderfuncstrictUnmarshal(data[]byte,vinterface{})error{decoder:json.NewDecoder(bytes.NewReader(data))decoder.DisallowUnknownFields()returndecoder.Decode(v)}五、嵌套结构与 JSON现实世界的数据通常是嵌套的。Go 通过嵌套结构体来映射嵌套 JSONtypeAddressstruct{Citystringjson:cityZipCodestringjson:zip_code}typePersonstruct{Namestringjson:nameAgeintjson:ageAddress Addressjson:addressHobbies[]stringjson: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{IDintjson:idCreatedAtstringjson:created_at}typeArticlestruct{BaseModel// 匿名嵌入字段会被提升Titlestringjson:titleContentstringjson: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);errnil{*fiFlexibleInt(num)returnnil}// 再尝试按字符串解析varstrstringiferr:json.Unmarshal(data,str);errnil{n,err:strconv.Atoi(str)iferr!nil{returnfmt.Errorf(无法解析为整数: %s,str)}*fiFlexibleInt(n)returnnil}returnfmt.Errorf(FlexibleInt: 无法解析 %s,string(data))}typeItemstruct{ID FlexibleIntjson:idNamestringjson:name}6.2 json.RawMessage延迟解析json.RawMessage本质上是一个[]byte别名它可以暂存一段原始 JSON 数据等确定结构后再解析typeEventstruct{Typestringjson:typePayload json.RawMessagejson:payload// 先原样保存}// 后续根据 Type 的值决定如何解析 Payloadswitchevent.Type{caseuser_login:varlogin LoginPayload json.Unmarshal(event.Payload,login)caseorder_create:varorder OrderPayload json.Unmarshal(event.Payload,order)}七、流式处理Encoder 和 Decoder当处理大量数据或网络流时使用json.Encoder和json.Decoder比一次性Marshal/Unmarshal更高效——它们直接操作io.Writer/io.Reader避免了中间大块内存分配。7.1 Encoder边写边编码typeLogEntrystruct{Timestampstringjson:timestampLevelstringjson:levelMessagestringjson: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/jsonfmtos)typeDBConfigstruct{Hoststringjson:hostPortintjson:portUserstringjson:userPasswordstringjson:password// 实际项目中不应该明文存储Databasestringjson:databasePoolstruct{MaxOpenConnsintjson:max_open_connsMaxIdleConnsintjson:max_idle_conns}json:pool}func(d DBConfig)DSN()string{returnfmt.Sprintf(%s:%stcp(%s:%d)/%s?charsetutf8mb4,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.Port0{returnnil,fmt.Errorf(配置项 port 不能为空)}returncfg,nil}练习 2API 响应包装器设计一个通用的 API 响应结构支持任意类型的数据载荷packagemainimport(encoding/jsonnet/http)var(ErrBadRequestAppError{Code:400,Message:请求参数错误}ErrNotFoundAppError{Code:404,Message:资源不存在}ErrInternalAppError{Code:500,Message:服务器内部错误})typeAppErrorstruct{Codeintjson:codeMessagestringjson:message}func(e*AppError)Error()string{returnfmt.Sprintf([%d] %s,e.Code,e.Message)}typeAPIResponsestruct{Successbooljson:successData json.RawMessagejson:data,omitemptyError*AppErrorjson: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; charsetutf-8)w.WriteHeader(http.StatusOK)returnjson.NewEncoder(w).Encode(resp)}// WriteError 向 http.ResponseWriter 写入错误响应funcWriteError(w http.ResponseWriter,errerror){appErr,ok:err.(*AppError)if!ok{appErrErrInternal}resp:APIResponse{Success:false,Error:appErr,}w.Header().Set(Content-Type,application/json; charsetutf-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 必须是 stringJSON 对象的键只能是字符串所以map[int]string无法直接序列化——要么换用map[string]string要么实现自定义序列化。[]byte 会被 Base64 编码JSON 不支持原始二进制[]byte类型会自动做 Base64 编码。