4、行情数据格式解析:JSON格式解析、Protocol Buffers(Protobuf)基础、处理嵌套数据结构

行情数据到了我们手里,第一件事就是把它“读懂”。

不同交易所、不同数据源,给出来的格式五花八门。但主流就两种:JSONProtobuf。JSON 你肯定见过,人类可读,调试方便。Protobuf 呢?二进制,体积小,解析快,适合高频场景。

我个人习惯是:开发阶段用 JSON,上生产换 Protobuf。为什么?因为 JSON 出问题了你能肉眼排查,Protobuf 一旦字段对不上,你连错在哪都不知道。

核心观点:行情解析的本质,就是把字节流变成内存里的结构体。JSON 和 Protobuf 只是序列化方式不同,最终都要映射到你的数据模型上。

4.1 JSON 格式解析:从字符串到结构体

JSON 解析,说白了就是 json.Unmarshal 一把梭?嗯,没那么简单。

我在项目中遇到过一个问题:某交易所的深度数据,一个字段叫 "bids",里面是二维数组 [[price, size], ...]。直接用标准库解析,你得定义一堆嵌套结构体,写起来很烦。

我的做法是:先定义好 Go 结构体,再用标签映射字段名。比如:

type Depth struct {
    Bids [][2]float64 `json:"bids"`
    Asks [][2]float64 `json:"asks"`
    Ts   int64        `json:"ts"`
}

这样一行 json.Unmarshal(data, &depth) 就搞定了。但要注意:浮点数精度问题。行情里的价格和数量,有时候是字符串,比如 "12345.6789"。直接转 float64 会丢精度。我建议用 json.Number 或者自定义类型来处理。

小技巧:如果 JSON 里某个字段可能不存在,用 omitempty 标签。如果字段类型可能变化(比如有时是字符串有时是数字),用 interface{} 再手动断言。但说实话,尽量避免这种设计,太坑了。

4.2 Protocol Buffers(Protobuf)基础:二进制才是王道

Protobuf 是什么?Google 搞的一套序列化协议。比 JSON 小 3-10 倍,解析速度快一个数量级。你想想看,每秒处理几万笔行情,用 JSON 解析 CPU 直接拉满,用 Protobuf 几乎没感觉。

先定义 .proto 文件:

syntax = "proto3";

message Tick {
    string symbol = 1;
    double price = 2;
    double volume = 3;
    int64 timestamp = 4;
    repeated Trade trades = 5;
}

message Trade {
    double price = 1;
    double size = 2;
    int64 ts = 3;
}

然后用 protoc 编译生成 Go 代码。解析的时候:

tick := &Tick{}
err := proto.Unmarshal(data, tick)

嗯,就这么简单。但坑在哪?

  • 字段编号不能改:一旦上线,1 就是 price,2 就是 volume。改了老数据就解析不了。
  • optional 和 default:Protobuf 3 默认所有字段都是 optional,没赋值就是零值。你没法区分「没发」和「发了 0」。
  • 嵌套太深影响性能:我见过有人把行情数据嵌套了 5 层,解析一次要递归好几回。没必要。

注意:Protobuf 不是银弹。如果你的行情数据量不大(比如每秒几百笔),JSON 完全够用。Protobuf 的维护成本(proto 文件管理、版本兼容)其实不低。我曾经在一个项目里因为 proto 文件版本没对齐,导致线上解析全乱套,排查了一整天才发现是字段编号冲突。

4.3 处理嵌套数据结构:扁平化 vs 保留层级

行情数据里嵌套结构太常见了。比如一个 tick 里包含多笔 trade,trade 里又有自己的子字段。怎么处理?

两种思路:

  1. 保留嵌套:直接映射成结构体里的结构体。代码清晰,但序列化/反序列化开销大。
  2. 扁平化:把嵌套字段拍平,比如 trade_pricetrade_size。解析快,但字段名会变得很长。

我个人建议:读的时候保留嵌套,写的时候扁平化。为什么?

读行情数据,你需要保持原始结构,方便后续逻辑判断。但写入数据库或者发送给下游,扁平化能减少存储空间和传输带宽。

举个例子,一个嵌套的 JSON:

{
  "symbol": "BTCUSDT",
  "trades": [
    {"price": 50000, "size": 0.1},
    {"price": 50001, "size": 0.2}
  ]
}

扁平化后变成:

{
  "symbol": "BTCUSDT",
  "trade_0_price": 50000,
  "trade_0_size": 0.1,
  "trade_1_price": 50001,
  "trade_1_size": 0.2
}

嗯,看着丑,但解析快。不过 trade 数量不固定时,扁平化就不好使了。这时候还是得用嵌套。

我的经验:嵌套深度控制在 2-3 层以内。超过 3 层,要么拆成多个消息,要么用 map 结构。别让解析器去递归 5 层,性能扛不住。

4.4 实战对比:JSON vs Protobuf 性能测试

我拿真实行情数据做过一次对比。100 万笔 tick 数据,每笔包含 10 个字段和 5 条 trade 嵌套。

指标 JSON Protobuf
数据大小 约 120 MB 约 28 MB
解析耗时 约 3.2 秒 约 0.4 秒
内存分配 约 450 MB 约 85 MB

差距很明显吧?但注意,这是纯解析时间。如果加上网络传输、磁盘 IO,Protobuf 的优势会被放大。不过 JSON 也有它的好处:调试方便。你可以在终端直接 curl 看数据,Protobuf 你得写解码器。

建议:开发环境用 JSON,测试环境用 JSON+Protobuf 双写,生产环境只用 Protobuf。这样既能保证开发效率,又能保证生产性能。

4.5 知识体系图:行情数据解析的核心逻辑

下面这张图,是我自己总结的行情解析流程。从原始字节流到最终的内存结构体,每一步都有坑,每一步都有优化空间。

行情数据解析核心流程 原始字节流 TCP/UDP/WebSocket 格式判断 JSON / Protobuf / 其他 JSON 解析 Unmarshal + 类型断言 Protobuf 解析 Unmarshal + 字段映射 嵌套结构处理 保留层级 / 扁平化 数据校验 & 清洗 精度、范围、完整性 内存结构体 关键优化点 • 预分配内存,减少 GC • 使用对象池复用结构体 • 避免反射,手动解析热点字段 • 嵌套深度控制在 3 层以内 • 浮点数用字符串传递

这张图里,我特别想强调数据校验这一步。很多人解析完就直接用了,结果价格是负数、时间戳是零值,跑策略的时候全乱套。我曾经就因为没校验时间戳,导致回测数据里混入了 1970 年的数据,查了三天才发现。

避坑指南:解析完数据后,一定要做三件事——检查字段是否在合理范围内、检查时间戳是否在可接受窗口内、检查嵌套数组长度是否超过预期。这三步能帮你挡住 90% 的脏数据。

好了,关于行情数据格式解析,核心就是这些。JSON 和 Protobuf 各有优劣,嵌套结构处理也有套路。记住一点:没有最好的格式,只有最适合你场景的格式。开发阶段别太纠结性能,先把功能跑通;上生产了再优化,用 Protobuf 替换 JSON,把嵌套拍平,把内存分配优化好。


公众号:蓝海资料掘金营,微信deep3321