diff options
Diffstat (limited to 'content')
| -rw-r--r-- | content/article/banality-of-go-err.md | 109 |
1 files changed, 107 insertions, 2 deletions
diff --git a/content/article/banality-of-go-err.md b/content/article/banality-of-go-err.md index 3cfb4f52..f58efa66 100644 --- a/content/article/banality-of-go-err.md +++ b/content/article/banality-of-go-err.md @@ -1,5 +1,5 @@ +++ -title = '地鼠错误之平庸' +title = '误之平庸' subtitle = 'The Banality of Go Error' slug = '' tags = [] @@ -11,5 +11,110 @@ toc = true 汉娜·阿伦特报道了阿道夫·艾希曼在耶路撒冷因组织屠杀犹太人受审的案件,在法庭上,她发现艾希曼并不是一个冷静且思维缜密的杀手,相反,“说话时间越长,越能感受到他表达力的匮乏与思考能力的缺失,他不会站在别人的立场思考问题,之所以无法同他交流,并不是因为他说谎,而是因为他周围围绕着坚固的壁垒,屏蔽他的言辞和他人的存在,从而帮他一并拒绝着真相。”她震惊于恶的平庸性(Banality of Evil)。 -艾尔特拉克和蓝色地鼠一起编写健壮的软件,在编辑器面前,他发现 `if err != nil` 并不是什么精巧的错误处理机制,相反,编程时间越长,越能感受到地鼠错误处理机制的缺失,他不会帮程序员抛出和拦截错误,之所以无法用条件判断以外的方式处理错误,并不是因为地鼠不想,而是因为已有的简单工具就足够提升程序的健壮性。他震惊于错误的平庸性——错误仅仅是包含 `Error()` 方法的[接口](/article/locally-remotely-unknowingly/)。 +艾尔特拉克和蓝色地鼠一起编写健壮的软件,在编辑器面前,他发现 `if err != nil` 并不是什么精巧的错误处理机制,相反,编程时间越长,越能感受到地鼠错误处理机制的缺失,他不会帮程序员抛出和拦截错误,之所以无法用条件判断以外的方式处理错误,并不是因为地鼠不想,而是因为已有的简单工具就足够提升程序的健壮性。他震惊于错误的平庸性——错误仅仅是包含 `Error()` 方法的[接口](/article/locally-remotely-unknowingly/)而已。 +<!--more--> + +## No Exception + +众所周知,嘲讽 Java 是这个博客的传统,所以今天的文章也以这个开始。在 Java 中,处理「错误」的方式是**抛出异常**。这里的异常(Exception)是指程序中可以预料到的问题,比如用户本应输入整数却输入了非数字字符,而错误(Error)在 Java 中指的是那些几乎无法恢复,会使程序退出的问题。 + +是的,术语开始混乱起来了,详见下表。 + +| | Java | Go | +| ---------------------------- | ----------------- | ----------------- | +| 可预期到的、程序应处理的问题 | 异常(Exception) | 错误(Error) | +| 没有预料到的、难以恢复的问题 | 错误(Error) | 崩溃(Panic[^1]) | + +为了方便讨论,在正式讨论 Go 语言之前,我会避免使用「错误」这个词,统一使用「异常」和「崩溃」两个词。老实说,「崩溃」这个词也不准确,Java 程序如果抛出 `Error` 不一定会终止程序,而 Go 程序如果 `panic` 也能够恢复,但姑且先这么用着吧。 + +Java 的异常和崩溃共用一套机制,他们甚至共用一个基类 `Throwable`(可抛出类)。Java 的异常和崩溃都是被抛出(throw)的,而抛出 `Throwable` 的函数的调用者可以接住(catch)被抛出的东西。不过 Oracle 的文档对 `Error`(崩溃)和 `Exception`(异常)两个子类的描述很有意思。 + +> The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch. + +{{< right "——[Exception - Java Platform SE 8](https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html)" >}} + +> An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. + +{{< right "——[Error - Java Platform SE 8](https://docs.oracle.com/javase/8/docs/api/java/lang/Error.html)" >}} + +简单来说,异常应该被接住,而崩溃不应该被接住。任何 `Throwable` 只要没被接住,就会使程序崩溃,提示 `Unhandled Exception`(未被处理的异常)——等等,你有没有发现,没有被接住的明明是 `Throwable`,为什么提示信息会是 `Unhandled Exception`?难道 `Error` 不也是 `Throwable` 吗? + +是的是的,我们已经发现 Java 让异常和崩溃共享同一个机制引发的问题了。异常和崩溃都可以被抛出,但区别在于崩溃不该被接住,而异常应该被处理。没有处理的异常会导致 `Unhandled Exception`,而本就不应该被处理的错误也会导致 `Unhandled Exception`。Java 还有一个 [`UncaughtExceptionHandler`](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.UncaughtExceptionHandler.html) 接口,用来处理线程里那些没有被接住的异常,我们来看看接口中包含的方法签名。 + +```java +interface UncaughtExceptionHandler { + void uncaughtException(Thread t, Throwable e); +} +``` + +啊,是的,代码里写的是 `Throwable`,可名字里明明是 `Exception` 呢。显然 `Throwable` 作为父类的抽象层次比子类 `Exception` 高,而熟悉 OOP 的程序员应该知道,父类不应该知晓子类存在,而一个为父类设计的接口竟然包含子类的名字,尽管没有直接依赖子类,但显然不是好的设计。更何况,我们已经体验到这种做法造成的迷惑和混乱了。 + +==如果崩溃不应该被接住,那它一开始就不应该与需要被接住的异常归属于同一基类==。如果子类之间的行为不同,就违反了里氏替换原则。*Always glad to find design flaws in Java.* + +我们一直在谈抛与接,是时候来看看代码里怎么写这两个东西了。「接」想必各位都很熟悉了,`try ... catch` 语句在 JavaScript 等语言中也能见到。 + +```java +try { + doSomething(); // 这个函数抛出异常 +} catch(IOException e) { + // 接住 IOException + // 处理这个异常 +} catch(Exception e) { + // Exception 是所有异常的基类 + // 处理剩下其他类型的异常 +} +``` + +如果不把 `doSomething()` 包裹在 `try` 里会怎么样呢?你会得到 `Unhandled Exception` 信息。为什么会这样呢?因为 `doSomething` 的方法签名声明了异常。 + +```java +void doSomething() throws Exception; +``` + +`throws` 关键词用在方法签名中,表示这个方法会抛出异常,而调用者应该抓住这个异常。如果声明了多种不同类型的异常,那么调用者应该抓住所有类型的异常,或者…… 只写一条 `catch(Exception e)`。如果调用了可能会抛出异常的方法却没有放在 `try` 里面,就会得到 `Unhandled Exception`。 + +真的吗?如果我就是不想要写 `try ... catch` 呢? + +那就让 Java 教会你踢皮球,把这个错误抛给你的调用者就好了! + +```java +// 这段代码会出错 +doSomething(); + +// 这段代码处理了异常 +try { + doSomething(); +} catch(Exception e) { + // ... +} + +// 这段代码向上抛出了异常 +void faireQuelqueChose() throws Exception { + doSomething(); +} +``` + +老实说,我写 Java 程序的时候就这么干过好几次,大不了最后这样写: + +```java +public static void main(String[] args) throws Exception { + // 想怎么抛怎么抛! +} +``` + +不过异常具体是怎么被抛出的? + +```java +void doSomething() throws Exception { + // 有不对劲的事情发生了 + // 赶紧抛出异常告诉调用者 + throw Exception("Something weird happened!") +} +``` + +嗯?这是个新的关键词吗?看起来是的,用在方法签名上告知调用者可能抛出的异常类型的关键词是 `throws`,而用在方法体内部抛出异常的语句是 `throw`。这个语句和一个标记了 `throws` 的方法调用在某种程度上是等价的,在方法体里写 `throw Exception()` 和可能会抛出异常的 `doSomething()`,都需要做异常处理,要么放在 `try ... catch` 里,要么往上抛让调用者处理。 + +原来 Java 程序员在一开始就要区分 `throw` 和 `throws` 这种细微的区别,怪不得我那个半吊子 Python 老师这么喜欢[纠结语言保留字](https://www.geedea.pro/article/ready-steady-go/#:~:text=%E8%BF%99%E4%BD%8D%E8%80%81%E5%B8%88%E5%9C%A8%E5%B9%BB%E7%81%AF%E7%89%87%E4%B8%8A%E5%B1%95%E7%A4%BA%E4%BA%86%20Python%20%E7%9A%84%E6%89%80%E6%9C%89%E4%BF%9D%E7%95%99%E5%AD%97%EF%BC%8C%E7%84%B6%E5%90%8E%E6%8F%90%E5%87%BA%E4%BA%86%E4%B8%80%E4%B8%AA%E5%8C%AA%E5%A4%B7%E6%89%80%E6%80%9D%E7%9A%84%E9%97%AE%E9%A2%98%EF%BC%9A%E4%B8%8A%E9%9D%A2%E8%BF%99%E4%BA%9B%E4%BF%9D%E7%95%99%E5%AD%97%EF%BC%8C%E5%93%AA%E4%BA%9B%E6%98%AF%20Python%20%E7%8B%AC%E6%9C%89%E7%9A%84%EF%BC%9F%E4%BA%8E%E6%98%AF%E6%8E%A5%E4%B8%8B%E6%9D%A5%E7%9A%84%E5%87%A0%E5%8D%81%E5%88%86%E9%92%9F%EF%BC%8C%E6%88%91%E4%BB%AC%E9%83%BD%E5%9C%A8%E8%A7%A3%E5%86%B3%E8%BF%99%E4%B8%AA%E9%97%AE%E9%A2%98%E3%80%82)! + +[^1]: Panic: 另译「运行时恐慌」 |
