@
bombless 我不理解你想说的是什么。“无法预期调用者会抛什么异常” 这个难道是你说的 “破坏了类型检查的原因?
可是异常之所以叫做异常,就是因为它存在 没有被捕获 的可能性,才叫异常的吧?异常没有被捕获就会在抛出点导致 crash ; 而 err 被意外地忽略了,然后继续执行,程序被预期以外的数据影响到别的地方的代码然后导致 crash ,这种情况不是更应该被警惕吗?
“返回错误,和抛出异常,根本就是不一样的结构;既不是顺序也不是分支,而是跳转,而返回错误依然是一个顺序结构之中。”
这句话你可以这么来理解: 在一个 try catch block 中间的代码,你可以认为是用一个独特的 goroutine 来运行的;这个 goroutine 与 try catch block 之外的上下文之间有一个 channel 用来通知异常。所以,当异常发生的时候, try catch block 中间的代码只是被中止了执行并没有自行退栈,而异常被传递了出去;返回 err 则不一样,函数被认为已经完成了随着 return 会退栈。
在局部上,你可以认为,在 try catch block 之间的每一行代码之间,编译器都会自动地加上等价于 golang 的 if err != nil { notify_exception(...) } 这样的代码(事实上 C++ 的编译器就是用类似的原理来实现的,会记住代码帧执行到哪一步) —— 或许这也是某些人认为 返回 err 和 try catch 除了手工增加防御性的代码之外,并没有本质的区别 ,然而 ——
notify_exception 把控制权交给了上层代码,这个上层代码并不仅仅是 try catch block 所在的上下文代码,还包括了调用栈上更靠近栈底的所有函数帧上的代码,就是说,那个用于通知异常的 channel 的 scope 是跨越 多个调用层次的;而没有 exception handling 机制的语言,例如 go 例如 C ,要做到这样的效果,就必须在相关的代码上层层防御——有没有嗅到一阵代码耦合的气味?
用 C 反而会更安全一点,因为 C 只能返回一个值,而调用者 *总*是* 有责任去判断这个返回值是否符合预期,譬如 bsd socket 的 recv 返回类型是 ssize_t , recv 的简单形式是告诉调用者接收了多少个字节(理应是非负数)然而 ssize_t 也说明了它会返回负数值 (异常发生),在这种情况下,其实就是一个弱化版的 checked exception 也就是非常接近 Java 的那种做法的本质——你*应该* 在调用的现场就处理好所有的异常,所以层层防御并没有在 C 里面成为现实,但即使是这样, C 代码依然非常容易耦合抽象程度很低。而 Java 的 checked exception 得益于其标准库的完备性,在编译的时候就已经强制保证了所有 exception 都要正确地被 catch 。 C 还是要看程序员个人修养。
然而 golang 用多值返回来描述异常,就打破了这种类似于 checked exception 隐含的强制性。多了一个 err 返回出来,就意味着懒惰的程序员总是有办法把责任往上扔,并且不怎么影响函数的返回值的设计,因为可以多值返回嘛——这跟懒惰的程序员总是接住所有的 exception 又不处理简直是一样的;
你甚至可以自己去看看 golang 自己的官方例子,产生了一个 err 之后,有什么东西可以阻止偷懒程序员或者做得昏头昏脑的程序员,继续去使用无意义的返回值去做任何事情吗?难道把 err print 出来事情就完了吗?
更坏的是,经过多层往上推卸责任之后,上层代码已经无法知道 扔上来的 err 到底是个什么鬼了——越是靠近调用栈栈底的函数,这个麻烦就越大,因为调用链越长,可能调用到的其他函数的范围越大。这个时候你必须猜这个 err 的所有可能性,想小心翼翼地处理这个 err 只能看运气,遗漏了处理某些具体类型的 err 的风险总是存在。
从这个角度来说, C++ 可以 throw 任何东西上去实际上也不是什么好的设计,尽管 C++ 还可以用 catch(...) 的语法来保证接住抛上来的任何东西,在需要阻止异常扩散的时候还有最后一招。
如果使用类似 C# python 等等的这种异常机制,首先是解除了处理 err 的耦合,并且总是保证,如果异常没有被恰当的接住就立即 crash , crash 的时候还可以保留现场,告诉 debug 的人是哪里导致的 crash ( golang 是做不到的,想好好地 log error 只能靠所有人都自觉),实际上这样才能更严格的要求调用者考虑清楚异常的类型;如果写的时候没有考虑清楚,那么跑的时候总是会的。
就算懒人程序员用类似于 catch(...) 这样的方式来偷懒绕过所有的检查和避免 crash ,它也没有办法把错误数据的影响扩散到其他地方,不会危害到其他地方的代码安全性。
综上所述, golang 的多值返回 err 的设计,就是事情没干得更好,倒是更容易让人做出更坏的事情出来——简直就是开历史的倒车。