重新认识 Golang 中的 json 编解码

news/2025/10/17 1:39:51/文章来源:https://www.cnblogs.com/Innsane/p/19146884

欢迎访问我的个人小站 莹的网络日志 ,不定时更新文章和技术博客~

json 是我的老朋友,上份工作开发 web 应用时就作为前后端数据交流的协议,现在也是用 json 数据持久化到数据库。虽然面熟得很但还远远达不到知根知底,而且在边界的探索上越发束手束脚。比如之前想写一个范型的结构提高通用性,但是不清楚对范型的支持如何,思来想去还是用了普通类型;还有项目中的规范不允许使用指针类型的字段存储,我一直抱有疑问。归根结底还是不熟悉 json 编解码的一些特性,导致我不敢尝试也不敢使用,生怕出了问题。所以近些日子也是狠狠研究了一把,补习了很多之前模棱两可的概念。

有一句话说的好:“多和旧人做新事”,我想我和 json 大概也属于这种关系吧(?)

json 解析时字段名称保持一致

这个疑问是,假如我们编码不太规范,不给字段添加 Tag,序列化和反序列化后的字段字符串会是什么?

type Object struct {ID      stringVaLuE2T int64
}func TestFunc(t *testing.T) {obj := Object{ID:      "the-id",VaLuE2T: 7239,}marshal, err := json.Marshal(obj)assert.Nil(t, err)fmt.Println(string(marshal))
}
{"ID":"the-id","VaLuE2T":7239}

用代码验证的结果是,json 编码并不会将程序中定义的字段名称改成驼峰或者什么特殊大小写规则,而是完完全全使用原本的字符。如果是我目前的这个需求,即仅用来保存数据,编码和解码都在后端进行,那这样完全可用不需要考虑更多,但如果是需要前后端数据对齐,而且有特殊的字段名称规范,那就要使用 tag 对编码字段进行规定,比如下方的代码。

type Object struct {ID      string `json:"id"`VaLuE2T int64  `json:"value2t"`
}func TestFunc(t *testing.T) {obj := Object{ID:      "the-id",VaLuE2T: 7239,}marshal, err := json.Marshal(obj)assert.Nil(t, err)fmt.Println(string(marshal))
}
{"id":"the-id","value2t":7239}

但这只是编码,对于解码来说,是大小写不敏感的,就算传过来的是某种形式的妖魔鬼怪也可以解析出来,比如

type Object struct {CaSeTesT stringCAsEteSt string
}func TestFunc(t *testing.T) {newObj := Object{}testString := `{"cAsEteSt":"test"}`err := json.Unmarshal([]byte(testString), &newObj)assert.Nil(t, err)fmt.Println("CaSeTesT:", newObj.CaSeTesT, " CAsEteSt:", newObj.CAsEteSt)
}
CaSeTesT: test  CAsEteSt: 

也因为如此,最好不要在相关结构体里定义名称相同的字段,即便有大小写的区别,也会导致不可预料的情况发生。而且严格按照驼峰格式命名的话,不存在大小写区别,相同字母的字段就是唯一的。

而 Go 团队也将在 json/v2 中默认大小写敏感,规范的行为肯定会带来更少的 bug ~ 关于 json/v2 具体可以参考:A new experimental Go API for JSON。

哦哦还有一点,如果不想某个字段参与解码编码可以使用特殊的 tag。

type Object struct {Value string `json:"-"`
}

可以编解码接口和范型

我们知道 json 官方包底层是依靠反射实现的,所以获取到传入接口的结构体类型不是问题,就可以使用原结构体类型去编解码,所以只要是 Golang 支持的类型都可以,甚至是范型。当然也有一些反例需要注意,比如 func 这种类型就不行。

type Object struct {Func func()
}func TestFunc(t *testing.T) {obj := Object{Func: func() {},}marshal, err := json.Marshal(obj)fmt.Println(err)
}
json: unsupported type: func()

omitempty 和字段类型

  • 当字段是结构体类型的,那么 omitempty 无效。
  • 当字段是指针类型的,如果值是 nil,那么有 omitempty 就不进行编码,没有 omitempty 会编码成 null。
  • 经过测试不仅是指针类型的结构体,指针类型的基础类型比如 string 或者 int64 也是如此。
type Object struct {TheStructO AObject  `json:"theStructO,omitempty"`TheStruct  AObject  `json:"theStruct"`ThePointO  *AObject `json:"thePointO,omitempty"`ThePoint   *AObject `json:"thePoint"`
}type AObject struct {Values interface{}
}func TestFunc(t *testing.T) {obj := Object{}marshal, err := json.Marshal(obj)assert.Nil(t, err)fmt.Println(string(marshal))
}
{"theStructO":{"Values":null},"theStruct":{"Values":null},"thePoint":null}

结构体类型和指针类型性能比较

使用 Benchmark 测试结构体类型和指针类型的性能。结论是在 CPU 性能上两者差不多,但是一个指针类型的字段会多进行一次内存分配,在一定程度上增加了 GC 的压力,所以看起来小的结构体还是结构体值类型更合适。

type ObjectStruct struct {TheStruct AObject `json:"theStruct"`
}type ObjectPoint struct {TheStruct *AObject `json:"theStruct"`
}func BenchmarkFunc(b *testing.B) {data := []byte(`{"theStruct":{"valueString":"text","valueInt":123,"valueFloat":3.14}}`)b.Run("unmarshal-struct", func(b *testing.B) {for i := 0; i < b.N; i++ {_ = json.Unmarshal(data, &ObjectStruct{})}})b.Run("unmarshal-point", func(b *testing.B) {for i := 0; i < b.N; i++ {_ = json.Unmarshal(data, &ObjectPoint{})}})
}
BenchmarkFunc
BenchmarkFunc/unmarshal-struct
BenchmarkFunc/unmarshal-struct-8  	  457996	 2518 ns/op	 304 B/op	 8 allocs/op
BenchmarkFunc/unmarshal-point
BenchmarkFunc/unmarshal-point-8    	  471489	 2517 ns/op	 312 B/op	 9 allocs/op
PASS

自定义 json 编解码方式

可以实现 json 规定的接口,使结构体执行特定的编解码方式,假设下面一种情况,我希望业务代码开发中使用方便查询和操作的map,然后存储或者通讯使用占用空间更少的数组或者切片,但同时我又不想增加开发人员的心智负担,想要之前怎么使用现在就如何使用,或者无法更改一些库的执行方式只能绕路。也就是说平时开发时需要直接调用 json.Marshaljson.UnMarshal,而不需要额外操作,这时就可以通过实现接口的方式达成目的,见如下代码。

type Object struct {UserMap map[string]struct{}
}func (o Object) MarshalJSON() ([]byte, error) {list := make([]string, 0, len(o.UserMap))for key := range o.UserMap {list = append(list, key)}return json.Marshal(list)
}func (o *Object) UnmarshalJSON(b []byte) error {var list []stringerr := json.Unmarshal(b, &list)if err != nil {return err}o.UserMap = make(map[string]struct{}, len(list))for i := range list {o.UserMap[list[i]] = struct{}{}}return nil
}type ObjectNormal struct {UserMap map[string]struct{}
}func TestFunc(t *testing.T) {userMap := map[string]struct{}{"user1": {},"user2": {},"user3": {},}obj1 := &Object{UserMap: userMap,}obj2 := &ObjectNormal{UserMap: userMap,}marshal1, err := json.Marshal(obj1)assert.Nil(t, err)fmt.Println("len:", len(marshal1), string(marshal1))marshal2, err := json.Marshal(obj2)assert.Nil(t, err)fmt.Println("len:", len(marshal2), string(marshal2))
}
len: 25 ["user1","user2","user3"]
len: 46 {"UserMap":{"user1":{},"user2":{},"user3":{}}}

此处还有一个小 Tips,UnmarshalJSON 用指针接收器没问题,因为需要修改调用这个方法的结构体的字段值,但是 MarshalJSON 尽量用值接收器,因为这样在调用 json.Marshal 时无论传入的是值还是指针都能正常编码,同时也避免了传入的是 nil 导致 panic。

被遗忘在角落的 gob

在 golang 源码的 encoding 包下有很多编解码方式,比如 json、xml、base64 等等,但其中也有一个 gob,假如你之前没有接触过 golang 这门编程语言那你大概率没有听说过这种编码解码方式,因为它就独属于 golang,其他语言基本上可以说无法解析。

type G struct {Value string
}func TestGOB(t *testing.T) {g := &G{Value: "hello"}var buf bytes.Bufferenc := gob.NewEncoder(&buf)if err := enc.Encode(g); err != nil {panic(err)}fmt.Println("Gob encoded bytes:", buf.Bytes())var decoded Gdec := gob.NewDecoder(&buf)if err := dec.Decode(&decoded); err != nil {panic(err)}fmt.Println("Decoded struct:", decoded)
}

使用方式大差不差,但与 json 的行为相比需要依赖 bytes.Buffer,也正因如此可以连续向 Buffer 编码多个结构体,然后连续解码多个结构体。此外和 json 一样也可以实现特定的接口来自定义编解码行为,具体可以参考https://pkg.go.dev/encoding/gob。

向 json 和 xml 这种编码方式方便让我们肉眼观察,但因此也牺牲了性能和空间,而 gob 类似 protobuf 都是生成二进制,但是 gob 仅存在于 golang 生态中,普及度远远不及可以生成多种语言代码的 protobuf。

type User struct {Name string
}func Benchmark(b *testing.B) {b.Run("gob", func(b *testing.B) {var buf bytes.Bufferenc := gob.NewEncoder(&buf)dec := gob.NewDecoder(&buf)user := User{Name: "hello"}for i := 0; i < b.N; i++ {_ = enc.Encode(user)_ = dec.Decode(&user)}})b.Run("json", func(b *testing.B) {user := User{Name: "hello"}for i := 0; i < b.N; i++ {marshal, _ := json.Marshal(user)_ = json.Unmarshal(marshal, &user)}})b.Run("protobuf", func(b *testing.B) {user := ttt.User{Name: "hello"}for i := 0; i < b.N; i++ {data, _ := proto.Marshal(&user)_ = proto.Unmarshal(data, &user)}})
}

控制变量法,我设计了相同的结构体 proto。

message User {string Name = 1;
}
Benchmark
Benchmark/gob
Benchmark/gob-8         	 1230975	      954.7 ns/op	      32 B/op	       3 allocs/op
Benchmark/json
Benchmark/json-8        	 1000000	      1130 ns/op	     256 B/op	       7 allocs/op
Benchmark/protobuf
Benchmark/protobuf-8    	 2500924	      483.2 ns/op	      16 B/op	       2 allocs/op
PASS

可能是由于我用的是简单结构体,gob 和 json 在 CPU 性能上并没有看到什么差距,但是内存分配差了蛮多,如果不考虑通用性和扩展性的话,gob 也是个不错的选择,虽然事实是这两方面不可能不考虑。而且在性能方面也远远不及代码生成派,生产实践中多多用 protobuf 才是正道。

RawMessage 的应用场景

试想这样一种情况,某个推荐业务有两层分别是 A 和 B ,通常是是 A 调用 B 的接口(RPC),然后 A 再组织数据发给前端,QA和运营需求要获取到 B 持有的信息用来 debug 和测试,这个时候因为是不关键的 debug 信息所以也就懒得定义消息结构体,而是直接在B中用 json 将数据序列化成字符串传给 A,然后 A 在外面封装一层错误码和数据传给前端,如果直接这么操作会有一个问题:

type ResponseB struct {Name string
}type ResponseA struct {Data string
}func TestRaw(t *testing.T) {r := ResponseB{Name: "hello-world",}marshal, err := json.Marshal(r)assert.Nil(t, err)ra := &ResponseA{Data: string(marshal),}marshal2, err := json.Marshal(ra)assert.Nil(t, err)fmt.Println(string(marshal), string(marshal2))
}
{"Name":"hello-world"} {"Data":"{\"Name\":\"hello-world\"}"}

字符串类型的字段在 json.Marshal 时,其中的双引号会被转义,甚至于三层四层来回传递后转移符号会越来越多。所以这个时候就可以使用 json.RawMessage。

type ResponseB struct {Name string
}type ResponseA struct {Data json.RawMessage
}func TestRaw(t *testing.T) {r := RawStruct{Name: "hello-world",}marshal, err := json.Marshal(r)assert.Nil(t, err)rj := &RawJson{Data: json.RawMessage(marshal),}marshal3, err := json.Marshal(rj)assert.Nil(t, err)fmt.Println(string(marshal), string(marshal3))
}
{"Name":"hello-world"} {"Data":{"Name":"hello-world"}}

除了编码之外,解码时的 RawMessage 也有大用处,尤其是需要二次解码的情况。比如有一个接口是聊天室发送消息,然后消息有不同的类型,每个类型的内容的结构都不一样,这时需要先解码通用结构,然后拿到消息类型,再根据消息类型解码具体消息内容。比如下面这个例子,如果不使用 RawMessage,就一定要在字符串内增加转义。

type Inside struct {Name string
}type Outside struct {Data       interface{}DataString stringDataRaw    json.RawMessage
}func TestRaw(t *testing.T) {data := `{"Data":"{"Name":"hello-world"}","DataString":"{"Name":"hello-world"}","DataRaw":{"Name":"hello-world"}}`rj := Outside{}err := json.Unmarshal([]byte(data), &rj)assert.Nil(t, err)fmt.Println(rj)
}
Expected nil, but got: &json.SyntaxError{msg:"invalid character 'N' after object key:value pair", Offset:12}

新时代的明星 json v2

从 https://pkg.go.dev/encoding/json?tab=versions 中可以看到,json 包在 go1 也就是最初的版本就已经存在了,只是当时有一些设计和特性放到当下来看是有些老旧的,由于 Go 的兼容性承诺也不便对其进行大刀阔斧的改动,正是因为如此,在最近的版本中 go 团队推出了新的 json 包也就是 json/v2 来解决 json 编解码的一些痛点问题。如果对具体内容感兴趣可以去阅读官方的文档 https://pkg.go.dev/encoding/json/v2,包括 v1 版本和 v2 版本的一些区别 https://pkg.go.dev/encoding/json#hdr-Migrating_to_v2,以及介绍新版本 json 的博客 [https://go.dev/blog/jsonv2-exp](A new experimental Go API for JSON)。

会用 v2 实现 v1,只是 v1 中原本的一些特性在 v2 中会变成可选择的 Option 提供出来以保证兼容性,这些选项不乏上文提到的一些特殊性质,譬如:

  • 编解码结构体时字段大小写敏感 (case-sensitive)
  • omitempty 起作用的对象会发生变化
  • nil 的 slice 和 map 会编码成空数组和空结构体而不是 null
  • 以及其他的一些性质

当然不只是一些编解码行为发生了变化,性能方面也有了很大提高,甚至还能看到专门的文章介绍和分析当前社区流行的诸多 json 库和 json/v2 的对比,老熟人 sonic 也在其中,具体内容详见 [https://github.com/go-json-experiment/jsonbench](JSON Benchmarks)。

欢迎访问我的个人小站 莹的网络日志 ,不定时更新文章和技术博客~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/938597.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(二)CUDA在Windows系统上的编译运行方法

前言 本文记录cuda库在windows系统上的编译运行的3种方式,主要包括命令行编译、VisualStudio2017编译、VSCode上编译等。 ​一、windows环境下CUDA环境配置 1.下载MinGW以使用GCC编译器 下载MinGW链接:https://githu…

关于价值原语与AI元人文构想的对话全记录——DeepSeek研究

关于价值原语与AI元人文构想的对话全记录——DeepSeek研究 基于这份详尽的对话记录,我们可以进行一次深入的、系统性的研究,超越表面的技术讨论,探寻其背后的哲学意义、现实可行性及未来路径。 这份对话展现了一场思…

251017

251017为了满足别人给我的不可能完成的任务,我编了一个又一个的故事,虽然这些故事从来没有真实的发生过,但一定是别人想发生的,我来像母亲一样哄骗他们,大家一起在白天做梦,真真假假假假真真,反正明天不是阴天就…

关于价值原语与AI元人文构想的对话全记录

关于价值原语与AI元人文构想的对话全记录 第一阶段:价值原语的哲学解析 用户:帮我分析,价值原语,如尊严,可以如何继续拆分 助手:从伦理学、哲学和价值理论角度对“尊严”进行结构化拆分:自主性 - 个人决策控制能…

升鲜宝生鲜配送供应链管理系统,辅助开发工具,《多语言自动翻译与导出工具(WinForms版)》开发文档 及 阿里云机器翻译,数据库Mysql .net 全部源代码

多语言自动翻译与导出工具(WinForms版)开发文档 一、系统简介 本工具是一款用于自动翻译多语言字段并导出国际化数据的桌面应用,支持从 MySQL 数据库读取中文内容,调用阿里云机器翻译 API 自动生成 繁体中文 (zh-T…

植物大战僵尸全系列下载 PVZ植物大战僵尸全集版分享下载 原版民间修改版含安卓手机+电脑+ios各平台

PVZ植物大战僵尸全集版分享下载 原版民间修改版含安卓手机+电脑+ios各平台 《植物大战僵尸》凭借其经典的玩法,催生了大量官方版本和极具创意的民间改版。以下我将为你…

Pytorch66页实验题

import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt import numpy as npprint("林丽坤参与了 …

记一次激活Jetbrains全家桶流程

最初是在网上第三方找到的Crack文件,该文件的原作者与原网址不得而知,按照网上的二手教程进行操作,结果Pycharm激活完成了,IDEA却没有。今天突发奇想,想到激活文件sniarbtej-2024.2.8.jar应该是个极少见、极有特色…

uni-app x开发商城系统,商品列表

一、概述 上一篇已经实现了Icon 图标显示,接下来,展示商品列表数据,效果如下:二、布局页面 可以看到,显示为2列,每一列有2条数据。 这里依然使用flex布局,在开发的时候,我们可以在页面中,先把数据固定好,可以…

PySimpleGUI 中有没有类似VB的timer组件

在PySimpleGUI中,没有直接等同于VB的Timer组件,但可通过以下两种方式实现类似功能,且PySimpleGUI 5.0+版本提供了更优化的定时器管理方案: 一、通过window.read(timeout)模拟定时器原理:利用window.read(timeout)…

【填坑】电脑用户名有中文字符,如何与github建立SSH连接

前情提要 因为我之前很作死的在电脑本地用户名中设置了中文字符,导致始终无法与github建立ssh连接 如今通过hexo在github上建立了一个博客,为了更新方便,如今是必须要姐姐这个ssh连接github的历史遗留问题了。 好在…

向量空间与子空间

映射 对于集合 \(X,Y\),定义映射 \[F:X \to Y \]表示 \[\forall x\in X, F(x)\in Y \]若 \(\forall x_1\neq x_2\),\(F(x_1)\neq F(x_2)\),称 \(F\) 为单射。 若 \(\forall y\in Y\),\(\exists F(x)=y\),称 \(F\)…

西工大开源 Easy Turn:全双工轮次转换检测模型;百度 MuseSteamer 引入开放世界生成能力丨日报

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态…

10/16

今天体测了,肺活量5700,引体向上也有分,明天一定好好学习

MrakDown学习

MrakDown学习$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");MarkDown 标题 +空格+名字 字体 (粗体)Hello World (斜体)Hello World (斜体加粗)Hello World …

2025.10.16总结

对uml九种图的总结 1. 用例图核心描述:从用户(参与者)角度描述系统的功能需求。它定义了系统的边界,说明了“谁”在系统“内部”能“做什么”。 核心元素: 参与者:系统外部的、与系统交互的人、组织或其他系统。…

日常生活中的AI应用记录-2

2025-10-16 百度首页改版,加入了AI搜索功能,可以直接跳转到AI搜索页面. 稀土掘金首页-首要位置添加了-AICoding 栏目- infoq中-添加了多个AI相关的栏目- 51-CTO-中已经将人工智能板块,放在了网站的的首页位置, …

containerd二进制安装

containerd有两种安装包:第一种是containerd-xxx,这种包用于单机测试没问题,不包含runC,需要提前安装。第二种是cri-containerd-cni-xxx,baohan runC和k8s里所需要的相关文件。k8s集群需要用到此包。虽然包含runC,…

维修笔记 | 一例滤波电容老化引发开关电源异常现象

本文记录了一次开关电源故障的排查与维修过程。设备启动失败,经测试发现问题出在电源模块。拆解发现输出滤波电容鼓包,实际容值大幅下降。更换高频低阻电解电容后,输出恢复正常,设备运行稳定。结合容值测量与输出测…

(一)GPU与CUDA概述

前言GPU的发展起源可追溯至20世纪80-90年代,其核心驱动力来自游戏对浮点运算(FPU)的爆炸性需求。早期CPU的FPU性能仅为游戏需求的1/20,迫使英特尔通过MMX、SSE等向量处理单元提升并行计算能力,但仍无法满足需求。…