GoSuda

Go 为何选择显式错误返回和 panic-recover,而不是 try-catch 异常抛出

By Rabbit Princess views ...

Go 语言有意不支持 try-catch,只支持 panic-recover 语法。这引起了许多习惯使用 try-catch 进行错误处理的开发者的不满。那么,为什么 Go 不引入 try-catch 呢?原因在于 try-catch 存在诸多问题。

try-catch-finally

try-catch(トライキャッチ)语句用于处理程序在执行过程中(运行时)可能发生的错误情况和异常情况。另外,finally 语句用于编写无论是否发生异常都必须执行的代码。

错误处理与责任

在 20 世纪 80 年代和 90 年代,错误处理非常简单。错误信息只有"软盘已满"、"驱动器中没有软盘"、"软盘没有写入权限"等几种,那个时代的开发者在发生错误时,会将该错误抛出(throw)到错误处理的位置,进行统一处理。在这种情况下,try-catch 语句运作得很有效率。

然而,随着时间的推移,情况发生了变化。业务逻辑变得复杂,出现了数据库和事务,需要通过网络调用大量 API 并解析大量的请求消息,甚至由于并发编程的出现,还必须在主线程以外的其他线程中处理错误。

错误已经复杂到无法在一处进行处理的程度,也无法让某一个地方对所有错误负责。正是在这里,try-catch 出现了严重的问题。

try-catch 的责任转嫁

简而言之,try-catch 是引发错误的主体将错误发生的责任(善后工作)推给他人的一种方式。被推给的对象可能是 catch 语句,也可能是其父方法,甚至是父方法的父方法的父方法的父方法……之中的某个人。换言之,在错误处理日益增多、日趋复杂的世界里,try-catch 所选择的方法正是"总会有人去处理的"。请看下面的代码。

1try {
2    data = readFile("hello.txt");
3    structuredData = parseData(data);
4    insertDBStatus(structuredData[1]);
5    startHTTPServer(structuredData[2]);
6} catch (Exception e) {
7    e.printStackTrace();
8}

上述代码的问题在于,不清楚由谁来处理 printStackTrace,也无法得知错误是在哪一段代码中发生的。而且 try 语句块中的逻辑越多,问题就越加严重。然而具有讽刺意味的是,开发越复杂,开发者就越沉迷于这种转嫁责任的 try-catch 语句,不再思考错误处理的问题,责任意识也随之淡化,最终忘记了错误与异常处理的本质。那么,golang 是如何解决这一问题的呢?

panic、recover

Go 的优点之一,在于它拥有多种机制,能够引导开发者不走弯路,从而成长为优秀的开发者。panic-recover 就是其中的一个例子。

panic-recover 乍看之下与 try-catch 并无太大区别,但在出现问题时,它们的不同之处在于不会将责任推向外部。在 Go 中发生 panic 时,不是将该值交给外部,而是应当在 panic 发生的位置予以解决。这使开发者承担起错误处理的责任,促使其更加深入地思考应当在何处、由谁、以何种方式处理该错误。这相当于在保障用户自主性的同时,也为其留下了思考的余地。

此外,panic 这一词语的选定也非常精妙。与 try-recover 不同,panic-recover 仅凭词语本身就给开发者施加了一种压力:只有在明确的错误情况下,而非仅仅是异常情况下,才应使用它。开发者自然会避免滥用 recover,只在必要之处使用。这对于在 Go 中编写简洁的代码有很大帮助。