proto3 里 0、空字符串和没设置是一回事?想把一个字段清零、想区分“没填”和“填了 0”该怎么设计接口?这篇用字节级的实验把这些坑一个个摆出来。
Req{} proto.Marshal 得到 0 字节;线上请求 DATA 帧载荷 [5] 字节;服务端看到 N=0
Req{N: 0, Msg: ""}(显式写了默认值) proto.Marshal 得到 0 字节;线上请求 DATA 帧载荷 [5] 字节;服务端看到 N=0
Req{N: 1} proto.Marshal 得到 2 字节;线上请求 DATA 帧载荷 [7] 字节;服务端看到 N=1proto3 的标量字段等于默认值时不序列化。接收方读到 0 时无法区分没设置和设成 0。
普通 int32,未设置 序列化 0 字节 ;接收方 Has(n)=false,值=0 普通 int32,设为 0 序列化 0 字节 ;接收方 Has(n)=false,值=0 optional int32,未设置 序列化 0 字节 ;接收方 Has(n)=false,值=0 optional int32,设为 0 序列化 2 字节 0800;接收方 Has(n)=true,值=0
给字段加 optional 会记录是否设置:设为 0 也会上线,接收方可以用 Has 判断。需要区分有无的字段用它。
未设置 序列化 0 字节 ;接收方 Has(n)=false,值=-1(-1 表示没有)
Int32Value{0} 序列化 2 字节 0a00;接收方 Has(n)=true,值=0(-1 表示没有)
Int32Value{7} 序列化 4 字节 0a020807;接收方 Has(n)=true,值=7(-1 表示没有)Int32Value 等包装类型靠子消息的存在表达有无,多 2 个字节,代码里多一层。是 optional 出现之前的做法。
普通 int32 n 补丁 {n: 0} 序列化为 0 字节;Merge 之后库里是 {msg=hello, n=7}
optional int32 n 补丁 {n: 0} 序列化为 2 字节;Merge 之后库里是 {msg=hello, n=0}用 Merge 合并补丁时,默认值不会覆盖已有值,清零做不到。用 optional 或 FieldMask 明确表达要改哪些字段。
inner 未设置:序列化 0 字节 ;Has=false;读 inner.value(不 panic)=0 inner 设置为空消息:序列化 2 字节 0a00;接收方 Has=true 生成代码里对 nil 的 *pb.Req 调 GetN()/GetMsg():0 / ""(不 panic)
子消息有存在性:未设置和设为空消息不同。生成代码的 getter 对 nil 安全,返回默认值。
默认:{"msg":"hi","big":"9007199254740993","x":1.5}
EmitUnpopulated:{"msg":"hi","n":0,"big":"9007199254740993","x":1.5}
JSON 里把 int64 写成数字 9007199254740993:err=<nil>;读到 {big=9007199254740993}
JSON 里把 double 写成 "NaN":err=<nil>;读到 {x=NaN}protojson 默认不输出零值;int64 输出成字符串,避免 JavaScript 丢精度。