protobuf 中 enum 类型默认值的一个坑
2017-12-05
在最近的项目中,我们使用 protobuf 的 enum 类型来表示接口的返回码,类似这样:
enum RtnCode {
SUCCESS = 0,
ERROR_1 = 1,
ERROR_2 = 2,
}
message AwesomeInterfaceRsp {
optional RtnCode rtn = 1 [default = SUCCESS];
// 其他字段...
}
开始一切正常,直到有一天服务端增加了一个错误码 ERROR_3 = 3
,我们发现,服务端返回这个新的错误时,客户端认为请求成功。
检查客户端代码:
if (rsp.rtn() == SUCCESS) {
log(kInfo, "SUCCESS");
// ....
} else {
log(kError, "Failed!");
// ....
}
似乎也没啥问题,可是总是执行到 SUCCESS
的分支。百思不得其解,我们决定把 rsp
先打印出来。服务端打印的是:
rtn: ERROR_3
客户端打印的是:
1: 3
嗯???
服务已经上线,改协议是不可能了,怎么补救呢?
思路 1:以后修改协议时,永远保证客户端先上线,服务端后上线。然而这样显然不可靠,现在刚出了问题记得,没准过两个月就忽略了。✘
思路 2:pb 不改,检查 has_rtn()
,这种情况会返回 false。这样同样不可靠,rtn
字段设置了 SUCCESS
的默认值,不排除服务端在成功的情况下没有 set。即使现在服务端保证总是显式设置,我作为客户端的开发,保险起见也不应该依赖这种行为。✘
思路 3:pb 小改,去掉默认值,检查 has_rtn()
。可行。✔
思路 4:pb 不改,客户端检查 UnknownFieldSet
。既然已经知道这个值被放到了 UnknownFieldSet
,我们可以检查到是否有未识别的枚举值:✔
template <typename MessageType>
bool CheckRtnCode(const MessageType& msg) {
if (msg.has_rtn())
return true;
const auto& unknown_fields = msg.unknown_fields();
for (int i = 0; i < unknown_fields.field_count(); ++i) {
const auto& field = unknown_fields.field(i);
if (field.number() == msg.kRtnFieldNumber) {
log(kError, "Unknown RtnCode found: %d; full message: %s",
int(field.varint()), msg.ShortDebugString().c_str());
return false;
}
}
return true;
}
当然,这只是权宜之计,客户端开发还是很容易忘了调用 has_rtn
/CheckRtnCode
。最好还是在设计 pb 时就考虑到这个问题,例如定义一个“未知错误”的代码作为默认值:
enum RtnCode {
UNKNOWN_ERROR = -1,
SUCCESS = 0,
ERROR_1 = 1,
ERROR_2 = 2,
}
message AwesomeInterfaceRsp {
optional RtnCode rtn = 1 [default = UNKNOWN_ERROR];
}
知道 pb enum 的这个特性以后,还可以得出一个推论:枚举类型绝对不要用 required
关键字!仍然是上面那个例子,如果 rtn
没有设置默认值,而是设置为 required
字段,那么客户端在 parse 时就会直接报错,根本不给补救机会。