json和Protobuf编解码性能对比

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 来调试查看。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容