Go语言 - 清晰,优雅,但却是错的

jopen 10年前

最近国外关于go语言的讨论很多,其中有一个论题是关于go语言里采用的错误码的异常处理模式和Java里的try-catch的模式孰优孰劣的问题。今天的这篇文章就涉及到这两种模式的对比比较。

并不是因为你看不见错误的产生就意味着它的不存在。

下面这段代码来自《C# programming》这本书,摘自讲述异常处理的章节。

try {    AccessDatabase accessDb = new AccessDatabase();    accessDb.GenerateDatabase();  } catch (Exception e) {    // Inspect caught exception  }    public void GenerateDatabase()  {    CreatePhysicalDatabase();    CreateTables();    CreateIndexes();  }

你是否注意到了这中写法是多么的清晰和优雅。

清晰,优雅,但却是错的。

假设在执行CreateIndexes()这个方法时有异常抛出。GenerateDatabase()方法并不捕获它,异常就会抛回给调用者,在那里被捕获。

但当异常从GenerateDatabase()方法中抛出时,重要的信息就丢失了:数据库创建的状态。捕获到异常的代码并不知道这创建数据库的哪一步出的错。需要删除掉索引吗?需要删除表吗?需要删除物理数据库吗?它不知道。

如果是在执行CreateIndexes()时出了错,你就永远丢失了一个物理数据库文件和里面的表。(因为这些文件会存放在硬盘上,它们存在哪里并不确定。)

在一个异常抛出模式的编程语言里写出正确的代码给人的感觉会更难,相比之下,在一个错误码模式的编程语言里给人的感觉会容易些,因为前者中任何东西都可能抛出异常,你必须时刻准备捕捉它。而后者很显然,当你在接收到错误码时才去检查发生的错误。在异常抛出模式中,你必须意识到任何地方都可能出现异常。

换句话说,在错误码模式中,如果有人没有捕捉到错误的发生,很显然是他没有检查错误码。但在异常抛出模式中,你不能很直观的从代码中发现是否已经有人捕捉到了错误,因为能产生错误的地方并不明显。

思考下面的代码:

Guy AddNewGuy(string name)  {   Guy guy = new Guy(name);   AddToLeague(guy);   guy.Team = ChooseRandomTeam();   return guy;  }

这个函数创建了一个新Guy,把他加入到俱乐部里,然后给他随机分配一个组。 不能再简单的操作了。

请记住:任何一行代码都可能产生错误。

如果”new Guy(name)”抛出了异常,会怎样?
哦,很幸运,我们还没有开始做什么,没有损失。
如果”AddToLeague(guy)”抛出了异常,会怎样?
我们新创建的“guy”就会被遗弃,但GC会把他清理干净。
如果”guy.Team = ChooseRandomTeam()”抛出了异常,会怎样?
哦偶,我们有麻烦了。我们已经把这个guy加入了俱乐部。如果有人捕获到了异常,他会发现俱乐部里的这个人不属于任何组。如果有一段代码是来遍历 俱乐部的所有会员,发现这个guy,哪个组的?这时就会得到一个NullReferenceException异常。因为他的组还没有初始化。

当你在写代码时,如果每行代码都会抛出异常,你是否明白每个异常都会产生什么样的后果?如果你想写出正确的代码,你就需要考虑到这些。

ok,如何修补这个问题?重新组织一下操作步骤。

Guy AddNewGuy(string name)  {   Guy guy = new Guy(name);   guy.Team = ChooseRandomTeam();   AddToLeague(guy);   return guy;  }

看起来是一个很微小的改动,但却对错误恢复产生巨大的影响。通过延后提交数据(把guy加入俱乐部),在构造这个guy过程中发生任何异常都不会产生任何的后续影响。会发生的事只是一个未构造完成的guy被丢弃了,最终会被GC清理掉。

通用设计原则:除非已经完备,不要提交数据。

当然,这个例子非常简单,因为在创建guy的步骤中没有其它的关联影响。如果在创建过程中出了什么问题,丢掉它就行了,让GC处理余下的事情。

在现实生活中,情况会麻烦的多。看看下面的代码:

Guy AddNewGuy(string name)  {   Guy guy = new Guy(name);   guy.Team = ChooseRandomTeam();   guy.Team.Add(guy);   AddToLeague(guy);   return guy;  }

跟上面我们改正过的函数一样,只是有人认为,如果在组里保持一个成员列表的引用会更有效率些,于是,你需要把自己add到你想加入到组里。这样做又会产生什么样的后果?

[本文英文原文链接:Cleaner, more elegant, and wrong ]

来自: 外刊IT评论 http://www.aqee.net/