关于错误码

初学编程,在C语言中定义错误码,是使用宏:

#define SUCCESS 0          //成功
#define FAILED  1          //失败

后来知道,用枚举更适合,因为宏的名声实在不好,而枚举可以帮你自动编号,减少错误码冲突,还有编译期校验,有调试器支持。

enum errorcodes {
    ERR_SUCCESS = 0, //成功
    ERR_FAILED = 1  //失败
}

不过C语言枚举比较烦的一个地方,就是要求不同枚举类型里的枚举名字必须不一样。所以不得不在每个枚举名前面加个前缀(比如上面的ERR_),来防止和其他枚举类型里面的枚举名字冲突。

还有就是最后一项枚举名后面不能有逗号,这也使维护枚举列表很麻烦。不过后来的C标准(好像是C99)进行了人性化改进,允许最末尾一项后面存在逗号。

C的枚举本质上还是整数,而Java的枚举更加灵活,可以为每个枚举项定义对应的数值和文本(以及其他数据类型):

enum ErrorCode
{
    SUCCESS(0, "成功"),
    FAILED(1,"失败"),
    ;

    private final int code;
    private final String msg;

    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() { return code; } 
    public String getMsg() { return msg; }
}

不过上面的msg主要还是起到一个注释的效果,通常会在资源文件中定义错误码对应的文本,以支持多语言和动态提示。

实际项目中不会让每个程序员按各自偏好的风格来定义各自的错误码,而是对错误码进行全局统一管理,比如所有代码共享一个公共的ErrorCode类,进行分段,并在开头添加注释说明:

/* 错误码划分,注意遵守
0              成功
00001-10000    公共错误
10001-20000    数据库错误
20001-30000    外部系统错误
30001-40000    模块自定义错误
    30001-31000    A模块或A特性
    31001-32000    B模块或B特性
*/

有的还会事先定义一些边界错误码来占位:

COMMON_START = 1,         //公共错误码开始
    //公共错误码在这里加
COMMON_END = 10000,       //公共错误码结束

DB_START_ = 10001,      //数据库错误码开始
    //数据库错误码在这里加
DB_END = 20000,          //数据库错误码结束

即使如此,不同的程序员在错误码的设计(粒度)上还会有分歧。比如:

有人只用一个“参数错误”来表示所有的接口入参问题的错误码
有人认为应该区分“必填参数为空”和“参数错误”
有人甚至给每个参数错误都设计不同的错误码,分别对应“A参数为空”,“A参数超长”,“A参数太短”,“B参数为空”,“B参数超长”等等。

一般还是认为“过犹不及”,差不多能让客户端明确错误原因就行。比如“参数错误”可以用同一个错误码,但加上动态的错误提示信息来表明具体是哪个参数错了,错在哪里。

但如果错误码定义太宽泛,一个错误码对应太多的失败场景,导致开发者在收到客户报错信息时,自己也是一头雾水,只能查看系统运行日志才能确定问题所在,那表示错误码可能需要细化了。(见到过一些极端的做法,是在错误码或者错误提示内,加入文件名和行号,用于直接找到源代码出错点,而不用查看系统日志)

微软甚至有个api指导文档,支持N层的错误码(其中错误码是字符串类型不是整型):https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses

{
  "error": {
    "code": "BadArgument",
    "message": "Previous passwords may not be reused",
    "target": "password",
    "innererror": {
      "code": "PasswordError",
      "innererror": {
        "code": "PasswordDoesNotMeetPolicy",
        "minLength": "6",
        "maxLength": "64",
        "characterTypes": ["lowerCase","upperCase","number","symbol"],
        "minDistinctCharacterTypes": "2",
        "innererror": {
          "code": "PasswordReuseNotAllowed"
        }
      }
    }
  }
}

曾经做过一个比较复杂的规则查询接口,为了让调用者知道具体是什么原因导致业务被禁止,给出了三段子错误码,比如:

子错误提示:根据[付款方式]判断,您的订单[用优惠券],因此不支持[退款]。
子错误码:1-3-1

其中,1=付款方式,3=用优惠券,1=退款,分别是三个不同维度下的枚举值。根据业务规则,存在成百上千种组合关系。如果客户想要获得全部错误码的文档,我们会遗憾地告诉他:“给不了,是动态生成的,没法穷尽”。(其实枚举项和它们对应的规则组合总数还是有限的,只不过枚举偶尔变,业务规则总在变。)

如果客户想要自己定制文案,提供给客户的客户,最不灵活而我们比较省事的做法,是我们把这些枚举值定义的表格都从数据库导出来,让客户自己去根据错误码生成文案。但以后我们每次增加枚举,都得通知客户。第二种方式是另外专门提供一个接口,供客户随时查询枚举值表或规则表,剩下的客户自己解决(这里也还要考虑变更如何让客户端知道,比如客户端每隔多久轮询刷新一次就可以了,不要求太实时,或者麻烦点让客户给个反向通知接口给我们调)。第三种是我们全包了,为不同客户定制不同的提示文案,根据不同的客户id进行返回。(文案模板如果放我们这里,又有耦合;如果让客户每次请求都携带,会浪费带宽。或者让客户只带模板id不带文案,额外给我们提供“根据id查询文案内容”的接口,这又太麻烦了。。。)

错误码的文档化有时还会遇到其他麻烦,特别是遇到严格的外部客户。

比如客户要求确切地给出每个接口能返回哪些错误码。虽然有swagger等工具,但好像也只能把所有全局错误码都堆上去,而不能判断代码的执行路径,自动提取对应的错误码,并生成文档。如果之前写代码时没有整理过错误码,这时恐怕就只能人工走一遍从接口进入的所有代码分支路径,找到对应的错误码。如果接口多,搞一次非常耗时。或者是改代码来简化错误码,在接口层对其他层的错误码做一次映射,比如把所有数据库错误或其他系统的500都映射成为一个“系统内部错误”再返回给客户端。不过这样做是否合适,对此大家的意见可能会有分歧。

或者客户要求在文档中指出哪些错误码是可以重试的(因为它不是用户触发的请求,要靠系统发起重试)。以HTTP的状态码为例,4xx是客户端错误,怎么重试都应该会得到相同的错误,所以不建议重试;而5xx是服务端错误,重试是有可能成功的。但像我们之前定义的全局错误码,不是按这个维度分段,而是按照模块或功能分段的,那就只能逐个错误码去挑出来,并进行标识说明。这也是费时的工作。要保证准确,还得检查错误码的各个使用点是否合理,而且后续新增和使用错误码都要考虑这个【是否支持重试】的维度。

另外,如果硬要死抠“是否可重试”,也会比较纠结。

比如客户说他们的重试时间是30分钟,而我们提供的某个接口业务规则,是“接口调用时间要在下单时间的1小时之后且在24小时内”,对应某一个错误码。实际上每个订单的下单时间都不同,如果收到请求时这个订单才下单45分钟,那么它重试后应该能成功。如果已经下单30小时了,那怎么重试都不行。

如果文档简单告诉客户,遇到这个错误码不能重试,会导致丢失不少业务,带来不少客诉。如果告诉客户全都能重试,他们可能又会投诉“每天很多重试都失败了,影响了我们的系统监控!解决一下!”

如果客户愿意配合改造,我们可以在响应消息中加上额外的时间信息,来帮助他们判断是否能够重试,但这种客户可能不多,因为太麻烦了,还会认为增加了与我们业务逻辑的耦合。如果客户一定要求我们来区分两种错误码(30分钟后重试能成功,30分钟后重试能成功),我们也会很尴尬:万一后续客户重试时间不是30分钟了怎么办,再加个系统参数来控制?如果后续这个参数要改动,谁来及时通知我们,这样是不是太耦合了?后续再接入其他重试时长不一样的客户系统呢?

当然这个例子是特例,比较极端,也许一般直接告诉客户不能重试就拉倒了。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值