不能直接用 json.marshal 存大字符串,因其会原样复制原始字节、不压缩、导致体积暴涨和内存过高;需通过自定义 marshaljson/unmarshaljson 接口实现按字段压缩与智能还原。

为什么不能直接用 json.Marshal 存大字符串
Go 的 json.Marshal 对超长字符串(比如几 MB 的日志片段、HTML 模板、Base64 编码的二进制内容)会直接复制原始字节,不做任何压缩,导致序列化后体积暴涨、内存占用高、网络传输慢。更关键的是,它无法感知内容是否“适合压缩”——纯 ASCII 文本和随机二进制数据混在一起时,盲目压缩反而可能膨胀。
真正需要的是:在 JSON 序列化过程中,对特定字段做「按需压缩 + 标记编码方式」,且解码时能自动识别并还原。
用 json.Marshaler 和 json.Unmarshaler 接口接管字段序列化
核心思路不是改全局 JSON 行为,而是让目标字段类型自己决定怎么编/解码。典型做法是定义一个包装类型:
type CompressedString struct {
raw string
}
func (c CompressedString) MarshalJSON() ([]byte, error) {
if len(c.raw)
- 必须显式判断长度阈值——
compressLZ4对短文本(如 "hello")压缩后反而更大,这是 LZ4/Gzip 的固有特性 - 结构体字段名要和 JSON key 严格一致,否则
json.Unmarshal会静默忽略 - 解码时要支持双格式兼容:新格式(带
compressed字段)和旧格式(纯字符串),否则存量数据会崩
选 LZ4 而不是 Gzip 的三个硬原因
实测对比 1–10MB 纯文本(日志、HTML、SQL dump):
-
LZ4压缩速度是gzip的 5–8 倍,CPU 占用低,适合高频写入场景 -
LZ4解压速度比gzip快 3–5 倍,对读多写少的服务更友好 -
LZ4支持无损 chunked 解压——万一 JSON 中只取某个字段,不用全解压整条记录(Gzip 不支持)
但注意:LZ4 压缩率通常比 gzip -6 低 10%–20%,如果存储成本远高于 CPU 成本,才值得切回 gzip。
嵌套结构里怎么让 CompressedString 正常工作
Go 的 JSON 包不会递归调用嵌套字段的 MarshalJSON,除非你手动触发。常见错误写法:
type LogEntry struct {
ID int `json:"id"`
Body CompressedString `json:"body"` // ✅ 这个会走自定义逻辑
Tags map[string]string `json:"tags"` // ❌ tags 里的 value 不会自动压缩
}
如果 Tags 的 value 也可能很大,必须显式封装:
type CompressedValue struct {
raw string
}
// 实现同 CompressedString 的 MarshalJSON/UnmarshalJSON
type LogEntry struct {
ID int `json:"id"`
Body CompressedString `json:"body"`
Tags map[string]CompressedValue `json:"tags"` // ✅
}
- 所有需要压缩的字段,类型必须是自定义类型(不能是
string或*string) - 指针类型(如
*CompressedString)会导致MarshalJSON不被调用——Go 会先看指针是否为 nil,再决定是否解引用 - 数组/切片元素类型也必须是自定义类型,
[]CompressedString才生效;[]string再怎么包都无效
512 很快就会不适用。上线前至少要跑一次采样分析,用真实数据算出 P95 长度再设阈值。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











