写在前面的话
当我们洋洋洒洒的写完一个项目或是一段代码片段后,第一件事就是运行它,看看它是否能正常工作。相信你都有过这样的经历,运行后总是不按照预算工作,或是运行时间数据类型不正确、或是不逻辑不符合要求。这些时候我首先想到的是找到错误,然后修正它。这样子看起来没有什么问题。那我们不妨思考一下,为什么会出现这样的问题呢?如果不是CURD代码。框架又是如何处理这些错误的呢?出现这些问题后我要如何举一反三防止这些问题再次发生呢?带着这些问题我有了如下的思考。
错误来源
这里我仅以PHP(8.0)代码举例。其它语言的错误场景类似。场景一:
$jsonString = '{"name": "John", "age": 30}';
$closure = json_decode($jsonString);
echo $closure->name;
这段程序你能看出什么问题吗?第一眼看上去好像并没有什么问题。运行也不会有任何问题。但是如果我们将$jsonString
改为`来源改为依赖网络请求、或是其它数据来源后,仔细思考一下,这段程序是否还能正常工作呢?比如:
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('https://api.example.com/data');
$jsonString = $response->getBody()->getContents();
$closure = json_decode($jsonString);
echo $closure->name;
如何修改
在这段程序中你能找出那些错误呢?联想一下你平时开发中遇到这样的问题后又是如何处理的呢?我们先来看构建的 client 请求。当请求发送,有可能遭遇网络问题、或是服务器问题。这些错误都是我们无法控制的。聪明的你肯定想到了。我需要捕获这些错误,然后进行处理。于是代码就变成了这样:
use GuzzleHttp\Client;
$client = new Client();
try {
$response = $client->get('https://api.example.com/data');
$jsonString = $response->getBody()->getContents();
$closure = json_decode($jsonString);
echo $closure->name;
} catch (\Exception $e) {
Log::error($e);
} catch(JsonException $e){
Log::error($e);
}
这样所有的异常都捕获了,然后根据我们业务在 catch 中进行处理比如发送错误通知管理员、或是记录日志等。目前看来已经把所有错误都捕获了。那我们接下在考虑我们程序中依赖的第三方API。我们理想的情况下希望对方返回 {"name": "John", "age": 30}
Json 字符。由于这部分是第三方API,我们无法控制对方返回给我们的数据百分百符合要求。这时就还需要增加数据类型检测。以及是否能通过JSON解码。基于这样的思考,我们就可以写出如下的代码:
use GuzzleHttp\Client;
$client = new Client();
try {
$response = $client->get('https://api.example.com/data');
$jsonString = $response->getBody()->getContents();
if (!is_string($jsonString)) {
throw new \Exception('Response is not a string');
}
if (is_null($jsonString)) {
throw new \Exception('Response is null');
}
$closure = json_decode($jsonString);
if (is_null($closure)) {
throw new \Exception('Response is not a valid JSON');
}
if (property_exists($closure, 'name')) {
throw new \Exception('closure not have name property');
}
echo $closure->name;
} catch (\Exception $e) {
Log::error($e);
} catch(JsonException $e){
Log::error($e);
}
OK,目前看来我们已经把所有可能的错误都捕获了。并且进行了处理。可以看到我们最终只是想要输出 John
。但是为了捕获这些错误,我们处理了大量的异常来保证我们的程序的健壮性。也许你会说我是君子约定并且在团队中也有相应的规范来约束一定要按规定的来返回,不需要这么麻烦。仔细想想如何要想实现一个健壮的程序,我们不能只靠这样的君子约定。毕竟只有有人参与的地方只要有可能出错的地方就一定会有错误。所以我在实现的相应的业务功能时我们都需要尽可能的处理完异常后在执行我们的业务逻辑。如果你有Go相关的开发经历,相信你对 go 的错误机制有很深的认识。在 golang 中所有的错误都需要迟早抛出并优先处理异常。刚开始接触时可能会觉得很不习惯,但是随着时间的推移,你会觉得这样的机制真的很棒。尽管在源代码中到处跟屎一样 error 随处可见。但是这样的好处是显而易见的。毕竟我们程序只有一小部分的时候是给人看大部分时间是运行在服务上。回看我们目前的代码有日志也有异常处理。即使在线上出了问题我们也有第一手日志来定位我们的错误。假设你代码写得飞起,没有进行任何异常或是只进行了不少量的异常处理。一天开发两三个模块。领导对你能力大为欣赏。突然有一天出了问题之后连查的地方都没有地方查。所以在日常工作千万不能嫌麻烦。
总结
我们的最终目标是希望写出健壮的程序。从这个角度出发我们需要做如下相应的工作:
- 对输出的参数进行类型检测,并记录
- 对程序有可能出错的地方进行异常捕获,并记录。进行防御性编程。
- 日志记录也需要有一定的规则。比如增加 RequestId, 那个模块,哪个方法,堆栈,时间,机器ID等详细信息等。
- 线上日志只保留 warning 和 error 级别日志。
这里只是我个人的一些思考,起一个抛砖引玉的作用。欢迎大家留言讨论。