面试题:try catch 对性能影响有多大
面试题
try catch 对性能影响有多大?
这是我在晚上休息时间刷视频时,偶然刷到的一个短视频中提到的一道所谓大厂面试题。
视频讲解者以JAVA语言为例进行了解答,他的观点主要是:
- 通过查看编译后的JAVA字节码可以发现,如果在没有抛异常的情况下,try…catch…语块几乎对性能没有任何影响;
- 如果抛出了异常,那么JAVA运行时就需要创建异常对象、记录调用栈、查找执行catch语块等操作,这些操作会比较明显地影响性能;
- 他建议的解决方案是用
return一个结果对象(例如{code: 0, data: foo}这种格式)来代替throw。
弹幕和评论区是以嘲讽居多,观众的观点主要有:
- 这是毫无营养的八股文,面试官是傻逼。
- 我做了性能测试,两种方案的性能差距,有,但可以忽略不计。
- 错误码的维护非常麻烦
- 返回错误码之后,上层、上上层每层都要进行if判断,本来业务逻辑已经走不下去了,可以直接中断掉。
- 绝大多数业务不需要考虑性能
- go 的 if err 是一坨屎
- rust:咱谁都别想好过
我们来尝试忽略那些情绪化或者欠缺深入思考的意见,尝试理性地分析这道题。
区分异常与错误
我在以前一篇技术分享会讲稿(Golang入门 & 一万种Goroutine的泄漏姿势)中的【语法8:错误处理】章节中,简单地介绍了Go语言的异常处理逻辑,即:不推荐 try/catch ,而是推荐返回一个错误值。
虽然大量的if err != nil成为了Go语言的最大争议点没有之一,但其实,凡是尽量在做高性能方向的语言,例如 rust 和 c,都是类似的解决方案。
rust语言使用Result对象来包装异常,随后的match语块其实与go语言的if err != nil是异曲同工的,不过rust借助一个?语法糖,立即就在很大程度上缓解了这个问题。虽然这点胜过了go,但同时又带来了另一个无法忽视问题:各个不同库的不同的Result定义之间难以兼容。
c语言我不太熟悉,我印象中主流的实践风格是int foo(callback),即无论如何只返回一个错误码,而成功的情况则通过传入的回调函数来继续执行。对Go语言来说这是一种被抛弃的糟粕,其关键点在于c语言只支持返回一个值,而go语言支持返回多个值。
由此可见,“用返回错误来代替异常中断”是高性能编程的共识,只不过目前还没有一种完美的实现方式罢了。
从这个角度来说,视频给出的结论并没有错。
那为什么又会有两种方式并存的情况呢?
“异常”(throw/panic)与“错误”(return)的本质区别是什么?
简单来说就是,可以预期到的、常见的导致逻辑不能继续执行的情况,应当是“错误”;而只有那些意外的、恶劣的情况才被算作“异常”。用Typescript可以举例为:
function run(param: number): void {
// 恶劣的、意外的,用异常。
if (typeof param === 'string') throw '已经声明了是number,为什么给我一个string?';
// ...
}
function run(param: Foo): Bar {
// 预料之内的、正常业务逻辑分支判断,用错误。
if (!param.user) return { code: -101, msg: '用户未登录' };
// ...
}
现实世界的妥协
理论如此,但是现实不一定要完全照搬。
例如REST、例如纯函数式编程,学院派可以在告诉你在理论上他们有多么多么好,但是现实世界中的最佳实践,却往往是在这些正确理论基础上做了一些妥协取舍之后的产物。
甚至可以说,这是程序员、或者说架构师的核心能力——他们能区分理论和现实,能根据实际情况做出取舍并达到一定条件下的最优解,而不仅仅是照本宣科。
就像你肯定不会拿rust或者c去写增删改查接口,也肯定不会拿Python或者JS去写缓存数据库一样,编程的世界没有银弹,每种技术都有他适用的场景。
在快速迭代的互联网业务中,使用throw抛出异常来简单粗暴地打断业务逻辑,在大多数情况下都是成本最优的方案。
——成本并不仅仅是CPU或者内存的占用,而更多体现在研发人工成本上。更别说部分前端业务甚至可以粗暴地认为CPU没有意义,因为用的是用户的硬件。在很多情况下,简单地给用户一个“系统异常”的提示,让用户刷新重试或者找客服就已经足够人性化了。
直接抛出异常打断,可以让研发人员少写很多 if err 的判断,可以让业务逻辑代码更紧凑,让业务流程更清晰,更有利于长期维护。
此外,也并不是说一个工程中只能用一种模式,更实际的方式是两种模式混用。例如在Go的博客文章Defer, Panic, and Recover中提到:
The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.
译文:Go的习惯做法是,即使在一个包的内部使用了panic,但它对外提供的接口一定会显式地返回代表错误的值。
换句话说,Go其实也在鼓励,在以返回错误作为主要模式基础上,可以适当运用panic在被封装的内部细节进行“偷懒”。
这是否是一道好的面试题?
作为面试题来说,其实这题本身的答案并不是重点,重点是候选人假设了怎样的场景、用什么手段和理论来分析可能的结果、是否有他亲身总结出的最佳实践经验,等等。
让我们回顾开头所说那个视频讲解者的分析过程,我认为按他那样回答的话,基本可以确定是可以得到比较高的面试评价的。我作为面试官,我看重的是,这位候选人他知道怎么分析字节码,他能辩证地给出正反两方面的分析,他知道“哪些操作比较重”这种常识,他的理论完善能够自圆其说,这些都是比答案本身正确与否更加重要的考察点。
但是还可以答得更好。
他的回答,其实从第一步“假设场景”开始,就把自己局限住了——他假设的是“JAVA语言、后端、高压业务场景”。如果他在答完上述内容之后,回到这个假设上来,拓展一些其他可能遇到的场景并做简要说明,我想,这就会是一份满分回答。