1、性能对比
我们先看下benchmark测试的性能对比,先整体感知以下,然后再分析原因。以下是测试代码:
代码放到xxx_test.go的文件中执行benchmark测试
package test
import (
"encoding/json"
"github.com/golang/protobuf/proto"
"github.com/mitchellh/mapstructure"
"model/pb"
"testing"
)
type NameInfo struct {
Name string `json:"name"`
AliasMS string `json:"aliasMS"`
AliasID string `json:"aliasID"`
AliasTH string `json:"aliasTH"`
AliasZH string `json:"aliasZH"`
AliasHI string `json:"aliasHI"`
AliasPH string `json:"aliasPH"`
}
func BenchmarkJsonMarshal(t *testing.B) {
n := NameInfo{
Name: "namenamenamename",
AliasMS: "一个很长很长很长的中文名字",
AliasID: "一个很长很长很长的中文名字",
AliasTH: "一个很长很长很长的中文名字",
AliasZH: "一个很长很长很长的中文名字",
AliasHI: "一个很长很长很长的中文名字",
AliasPH: "一个很长很长很长的中文名字",
}
t.StartTimer()
for i := 0; i < t.N; i++ {
_, err := json.Marshal(&n)
if err != nil {
t.Error(err)
return
}
}
t.StopTimer()
}
func BenchmarkJsonUnmarshal(t *testing.B) {
n := NameInfo{
Name: "namenamenamename",
AliasMS: "一个很长很长很长的中文名字",
AliasID: "一个很长很长很长的中文名字",
AliasTH: "一个很长很长很长的中文名字",
AliasZH: "一个很长很长很长的中文名字",
AliasHI: "一个很长很长很长的中文名字",
AliasPH: "一个很长很长很长的中文名字",
}
data, err := json.Marshal(n)
if err != nil {
t.Error(err)
return
}
t.StartTimer()
for i := 0; i < t.N; i++ {
err := json.Unmarshal(data, &n)
if err != nil {
t.Error(err)
return
}
}
t.StopTimer()
}
func BenchmarkPbMarshal(t *testing.B) {
n := pb.NameInfo{
Name: "namenamenamename",
AliasMS: "一个很长很长很长的中文名字",
AliasID: "一个很长很长很长的中文名字",
AliasTH: "一个很长很长很长的中文名字",
AliasZH: "一个很长很长很长的中文名字",
AliasHI: "一个很长很长很长的中文名字",
AliasPH: "一个很长很长很长的中文名字",
}
t.StartTimer()
for i := 0; i < t.N; i++ {
_, err := proto.Marshal(&n)
if err != nil {
t.Error(err)
return
}
}
t.StopTimer()
}
func BenchmarkPbUnmarshal(t *testing.B) {
n := pb.NameInfo{
Name: "namenamenamename",
AliasMS: "一个很长很长很长的中文名字",
AliasID: "一个很长很长很长的中文名字",
AliasTH: "一个很长很长很长的中文名字",
AliasZH: "一个很长很长很长的中文名字",
AliasHI: "一个很长很长很长的中文名字",
AliasPH: "一个很长很长很长的中文名字",
}
data, err := proto.Marshal(&n)
if err != nil {
t.Error(err)
return
}
t.StartTimer()
for i := 0; i < t.N; i++ {
err := proto.Unmarshal(data, &n)
if err != nil {
t.Error(err)
return
}
}
t.StopTimer()
}
需要先准备proto文件
syntax = "proto3";
package pb;
option go_package = "pb";
message NameInfo {
string name = 1;
string aliasMS = 2;
string aliasID = 3;
string aliasTH = 4;
string aliasZH = 5;
string aliasHI = 6;
string aliasPH = 7;
map<string, string> other = 8;
uint32 order = 20;
}
记得先生成pb.go的文件。
注意好benchmark测试代码中pb包引用的路径。然后我们看benchmark测试的结果
unmarkshal解码测试的结果
marshal测试的结果
🧪 Benchmark 支持
项目 | JSON | Protobuf |
---|---|---|
ns/op (耗时) |
2194 | 428 |
B/op (内存) |
520 | 304 |
allocs/op |
11 | 7 |
说明:
- Protobuf 快 4~5 倍
- 内存减少约 40%
- GC 负担更轻
✅ 总结
对比项 | JSON | Protobuf |
---|---|---|
速度 | 慢(反射 + 文本解析) | 快(静态类型 + 二进制) |
内存分配 | 多 | 少 |
可读性 | ✅ 好 | ❌ 差(但高效) |
使用场景 | Web API、日志等 | 内部通信、高性能服务、微服务 |
从结果中我们看出proto的速度比json快5倍多,占用内存比json少了一半。
注意,我们unmarkshal测试的时候,同时也包含了Marshal的代码,因为proto的Marshal本身就比json的性能要快,所以实际的结果差距会更大。
🔧 2、性能更好、内存占用更小的原因
我们从 设计原理 和 运行机制 两方面来对比分析:
<1> 编码格式:紧凑 vs 可读
特性 | JSON | Protobuf |
---|---|---|
数据格式 | 文本(字符串) | 二进制 |
可读性 | ✅ 可读 | ❌ 不可读 |
空间利用 | ❌ 冗余(字段名也编码) | ✅ 高度压缩(使用编号代替字段名) |
- JSON 每次都要写入字段名,如
"name":"Alice"
。 - Protobuf 使用数字标签(如
1: "Alice"
),并采用变长编码(varint),压缩得非常小。
🔍 示例:
一个简单的结构体在 JSON 中可能是 100 字节,在 Protobuf 中可能只有 30 字节。
<2> 解析方式:反射 vs 生成代码
特性 | JSON | Protobuf |
---|---|---|
解码机制 | 反射 + 动态类型 | 静态编译后的结构 |
类型推导 | 运行时 | 编译时 |
错误开销 | 可能多(字段缺失、类型不符) | 少(严格类型校验) |
- JSON 使用
reflect
来解析字段 —— 慢; - Protobuf 是生成
.pb.go
文件,直接访问结构字段,无需反射 —— 快。
<3> 内存分配:临时对象 & 拷贝
- JSON 解码时会频繁创建中间字符串、map、interface 等对象,导致 更多的内存分配(alloc)和 GC 压力。
- Protobuf 直接操作内存中的结构体指针,几乎无中间对象创建,极少分配内存。
<4> 编解码流程对比
步骤 | JSON.Marshal | proto.Marshal |
---|---|---|
遍历字段 | 用反射 | 用生成代码 |
判断类型 | 动态检查类型 | 静态类型,直接访问 |
转换为字符串 | 必须 | 无 |
写入缓冲区 | 写入字段名+值 | 写入字段号+值,变长编码 |
分配内存 | 较多 | 极少 |
3、使用场景分析
特性/指标 | JSON(encoding/json ) |
Protobuf(google.golang.org/protobuf ) |
---|---|---|
编码格式 | 文本格式,易读 | 二进制格式,高度紧凑 |
可读性 | ✅ 极强,可直接打印或展示 | ❌ 差,人类不可读 |
性能(速度) | ❌ 慢,反射开销大 | ✅ 快,生成代码执行效率高 |
内存占用 | ❌ 高,分配多(字符串、map等) | ✅ 低,紧凑编码和少分配 |
可扩展性 | 一般,字段不兼容时容易出错 | 很好,支持向前向后兼容(默认值/字段编号) |
依赖性 | 无依赖,Go 原生 | 需要 .proto 文件 + 生成工具 |
接口友好(Web) | ✅ 标准 REST 格式 | ❌ 需要中间件,如 gRPC |
跨语言支持 | 一般(需标准约定) | ✅ 极好(多语言官方支持) |
二进制支持 | ❌ 差(需 base64 编码) | ✅ 原生支持嵌套结构和二进制字段 |
动态结构支持 | ✅ map[string]interface{} 灵活 |
❌ 编译时确定结构 |
所以总结就是json用于面向人类可读、前端通信、不强调极致性能的场景。protobuf用于面向机器对机器通信(M2M)、高性能需求、带宽受限环境的场景。
🧪 小技巧
在性能关键路径上,可先用 JSON 做开发,后期优化时替换为 Protobuf。
Go 中可以使用 easyjson 等工具提升 JSON 的性能,但仍无法与 Protobuf 匹敌。
Protobuf 编码内容不适合直接打印调试,可以使用 protojson 来调试查看。