正确的错误处理是困难的
正确的错误处理是困难的
原文:https://medium.com/hackernoon/correct-error-handling-is-hard-307ea72759c7
就在最近,我在使用谷歌云平台的 Go 客户端时,被提醒了正确的错误处理是多么困难。
我写的代码是这样的:
一切似乎都很好,除了my-object没有被创造出来!
更仔细地检查整个事情,结果是writer.Close()(第 2 行)返回一个错误,说客户端没有被授权。
我的第一反应是,gcpClient.Bucket(“my-gcp-bucket”).Object(“my-object”).NewWriter(context.TODO())或io.Copy(writer, src)应该已经返回了那个错误。但是仔细想想,我意识到这是不可能的,此外,一个 I/O 对象只有在关闭它的时候才返回一个错误是完全没问题的。
关键是,I/O 对象可能会在以下情况下返回错误:(1)获取它时,(2)读取/写入它时,或者(3)关闭它时。这是合同的一部分。
所以常用的defer ioObject.Close()依赖于假设从Close()返回的错误可以忽略。一般来说这是不正确的。例子是上面例子中的作者,甚至是一个简单的os.File。
来自https://linux.die.net/man/2/close:
不检查 close ()的返回值是一个常见但严重的编程错误。很有可能先前的 写 (2) 操作上的错误是在最后的关闭()时首次报告的。关闭文件时不检查返回值可能会导致数据无声丢失。
不幸的是,Go 博客——推迟、恐慌和恢复建议:
如您所见,没有处理任何Close()错误。
正确的错误处理
如果有正确的错误处理,这个版本会是什么样子?像这样:
什么变了?我们移除了defer调用,并在所有代码路径中用显式的Close调用替换它们。现在,当在Copy(第 13 行)期间发生错误时,我们返回一个错误,但是关闭之前的两个文件(第 15–17 行)。我们忽略来自Close的错误,因为这要么成功,要么失败,但在这一点上,它可能只是已经发生的前一个错误的结果。复制完成后,我们关闭文件,但是显式地处理这些调用的错误,不要忽略它们。
这很啰嗦吗?是的,它是!当您正确处理错误时,代码会变得冗长。当生产中出现问题时,它也是你的生命线。
不幸的是,这段代码也有一些问题:
- 它包含许多重复的行
- 很难把
Close叫逻辑正确 - 当某个地方发生紧急情况时,它不会关闭文件
我们能做得更好吗?
更安全的方法
原来有一个小帮手,我称之为SafeCloser,可以帮忙:
这实际上只是一个安全措施,所以你想关闭一个Closer多次。很多Closer的实现已经是幂等的了,但是不能保证。SafeCloser是保证。
它是如何工作的?对于每个Closer,你只需创建一个同伴SafeCloser,并通过这个SafeCloser关闭你的Closer。
我们的CopyFile功能示例:
请注意我是如何在每个defer块之前声明一个SafeCloser并在其中使用的(第 6、7、13、14 行)。当我们显式关闭文件时,在底部(第 21 和 26 行)使用相同的SafeCloser。
我们避免了重复行,并正确处理了混乱。
为了避免误用,我故意没有用SafeCloser实现io.Closer接口。而不是打算把任何io.Closer包起来,传来传去,随便关几次。相反,这是一个非常具体的用例,根据您是处于失败模式还是成功模式,您会以不同的方式处理Close呼叫。
摘要
正确的错误处理是困难的。大多数情况下,这是不正确的。像上面的SafeCloser这样的小助手可以帮助您正确处理错误,同时尽可能保持简单。
如果你喜欢这个帖子,你可能也会对我的后续帖子感兴趣,“ 纠错很难,Part 2 ”。
黑客中午是黑客如何开始他们的下午。我们是这个家庭的一员。我们现在接受投稿并乐意讨论广告&赞助机会。
要了解更多信息,请阅读我们的“关于”页面、在脸书上给我们点赞/发消息,或者简单地说, tweet/DM @HackerNoon。