Errors are values
原文地址 https://blog.golang.org/errors-are-values
Go程序员之间(特别是这些刚接触Go语言的新人)一个常见的讨论点是如何处理错误。谈话经常变成为对如下代码序列出现次数的感叹。
If err!=nil{
return err
}
我们最近扫描了我们可以找到的所有开源项目,并发现这个代码段每页或每两页只出现一次,比你相信的要少。然而,如果这种看法仍然存在:所有时候都必须输入
If err!=nil
一定有些地方是错误的,明显的目标就是Go本身。
这是不幸的,误导的,很容易纠正的。也许正在发生的事情是新的程序员问道“如何处理错误?”,学习这种模式,并停止在那里。在其他语言中,可能会使用try-catch块或其他这样的机制来处理错误。因此,程序员认为,当我用旧的语言使用try-catch时,我将在Go中输入err!=nil。随着时间的推移,Go代码收集了很多这样的片段,结果感觉很笨拙。
无论这个解释是否合适,很明显,这些Go程序员错过了关于errors的基本观点:错误是值(Errors are values)
值可编程,并且由于错误是值,所以可以对错误进行编程。
当然,一个涉及错误值的常见语句是测试它是否为nil,但是还有无数的其他东西可以使用错误值,并且应用某些其他的东西可以使你的程序更好,消除了大量样板(如果每个error通过一个死记硬背的if语句来判断)。
这是一个简单的例子,来自bufio包的Scanner类型。它的Scan方法执行底层I/O,这当然可能导致错误。然而Scan方法根本不会显示错误。相反,它返回一个布尔值,并且在扫描结束时用一个单独的方法报告是否发生错误。客户端代码如下所示:
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
当然,这里有个对err的nil检查,但它似乎只执行一次。Scan方法可以被定义为
func (s *Scanner) Scan() (token []byte, error)
然后示例用户代码可能如下:(取决于如何取得token)
scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
这不是很大的不同,但有一个重要的区别。在此代码中,客户端必须在每次迭代时检查错误,但在实际的Scan API中,将错误处理从关键的API元素中抽象出来,在token上迭代。使用真实的API,客户端代码感觉更自然:循环直到完成,然后判断错误。错误处理不会干扰控制流。
看内部发生了什么,当然是,一旦Scan遇到I/O错误,它会记录它并返回false。一个单独的方法Err会在客户端询问时报告错误值。简而言之,这与把 if err != nil放到任何地方或要求客户端在每个token后检查错误的做法是不一样的。它是用错误值进行编程。简单的编程,是的,但仍然编程。
值得强调的是,无论设计如何,程序检查errors是非常重要的,然而它们是暴露出来的。这里的讨论不是关于如何避免检查错误,它是关于使用语言来优雅地处理错误。
当我在东京参加2014年秋天的GoCon时,出现了重复性错误检查代码的主题。一个热心的gopher,在Twitter上的@jxck_,回应了熟悉的关于错误检查的感叹。他有一些代码看起来像这样:
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on
这是非常重复的。在真正的代码中(更长的),有更多的,所以使用一个helper函数重构是不容易的,但是在这种理想的形式中,一个关于错误变量的函数将有帮助
var err error
write := func(buf []byte) {
if err != nil {
return
}
_, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
return err
}
这个模式运作良好,但是需要在每个执行写入操作的函数中有一个闭包;一个单独的helper函数使用起来是笨拙的,因为err变量需要在调用之间保持。
我们可以通过借用Scan方法的想法来使这个更整洁,更通用和可重用。我在讨论中提到了这种技术,但是@jxck_没有看懂如何应用它。经过长时间的交流,收到语言障碍的阻碍,我问道我是否可以借用他的笔记本电脑,并通过键入一些代码来给他看。
我定义了一个名为errWrite的对象,如下所示:
type errWriter struct {
w io.Writer
err error
}
并给它一个方法,write。它不需要具有标准的Write签名,小写部分来突出显示区别。write方法调用底层Writer的Write方法,并记录第一个错误以供将来参考:
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
一旦发生错误,写入方法将变为无效,但是会保存错误值。
给出errWrite类型及其写入方法,可以重构以上代码:
ew:=&errWriter {w:fd}
ew.write(p0 [a:b])
ew.write(p1 [c:d])
ew.write(p2 [e:f])
// 等等
如果ew.err!= nil {
返回ew.err
}
这更干净,甚至与使用闭包相比,也使得实际的写入序列在页面上更容易看到。没有任何杂乱。使用错误值(和接口)编程使代码更好。
同一个软件包中的其他一些代码很可能会建立在这个想法上,甚至直接使用errWriter。
另外,一旦errWriter存在,还有更多的可以帮助,特别是在较少人为的例子中。它可以累加字节数。它可以将写入合并到单个缓冲区,然后可以原子传输。以及更多。
实际上,这种模式经常出现在标准库中。Archive/zip和net/http包使用它。这个讨论更为突出,bufio包的Writer实际上是一个errWriter想法的实现。虽然bufio.Writer.Write返回一个error,这主要是关于遵守io.Writer接口的形式。bufio.Writer的Write方法与上面的errWrite.write方法是一样,Flush报告错误,所以我们的例子可以这样写:
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
return b.Flush()
}
这种方法有一个明显的缺点,至少对于某些应用程序:无法知道错误发生之前完成了多少处理。如果这些信息很重要,则需要采用更细粒度的方法。通常,尽管如此,一个在末尾的完全或没有检查就足够了。
我们只看了一种避免重复错误处理代码的技术。请记住,使用errWrite或者bufio.Writer不是简化错误处理的唯一方法,而且这种方法不适用于所有情况。然而,重要的教训是errors是值,Go编程语言的全部功能可用于处理它们。
使用语言来简化错误处理。
但记住:无论你做什么,总是检查你的errors。
最后,关于我与@jxck_互动的完整故事,包括他录制的一点视频,请访问他的博客。
Errors are values的更多相关文章
- 再说表单验证,在Web Api中使用ModelState进行接口参数验证
写在前面 上篇文章中说到了表单验证的问题,然后尝试了一下用扩展方法实现链式编程,评论区大家讨论的非常激烈也推荐了一些很强大的验证插件.其中一位园友提到了说可以使用MVC的ModelState,因为之前 ...
- 【干货】如何通过OPC自定义接口来实现客户端数据的读取?
上篇博文分享了我的知识库,被好多人关注,受宠若惊.今天我把我在项目中封装的OPC自定义接口的程序分享一下.下面将会简单简单介绍下OPC DA客户端数据访问,以及搭配整个系统的运行环境. OPC(OLE ...
- 【GoLang】GoLang 官方 对 error 处理的意见
The Go Blog Errors are values 12 January 2015 A common point of discussion among Go programmers, esp ...
- 【GoLang】panic defer recover 深入理解
唉,只能说C程序员可以接受go的错误设计,相比java来说这个设计真的很差劲! 我认为知乎上说的比较中肯的: 1. The key lesson, however, is that errors ar ...
- MVC 登录认证与授权及读取登录错误码
最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来. 十年河东十年河西,莫欺少年穷 学无止境,精益求精 最近在自学MVC,遇到的问题很多,索性一点点总结下 ...
- MVC中Model,不仅仅只是数据的传递者
在Model使用的时候很多人回向以前写三层架构一样使用它,将Model作为数据的传递者. 比如常见的写法 public int Id { get; set; } public int RoleId { ...
- zookeeper 学习笔记 (C语言版本)
1.zookeeper简介 zookeeper是Hadoop的子项目,在大型分布式系统中,zookeeper封装好了一些复杂易出错的服务,提供简单易用的接口,给使用者提供高效稳定的服务.这些服务包括配 ...
- redux-form的学习笔记二--实现表单的同步验证
(注:这篇博客参考自redux-form的官方英文文档)左转http://redux-form.com/6.5.0/examples/syncValidation/ 在这篇博客里,我将用redux-f ...
- 小而美的 React Form 组件
背景 之间在一篇介绍过 Table 组件< React 实现一个漂亮的 Table > 的文章中讲到过,在企业级后台产品中,用的最多且复杂的组件主要包括 Table.Form.Chart, ...
随机推荐
- ShoneSharp语言(S#)的设计和使用介绍系列(4)— 入门概述
ShoneSharp语言(S#)的设计和使用介绍 系列(4)- 入门概述 作者:Shone 声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp. ...
- Ceph luminous 安装配置
Ceph luminous 安装配置 #环境centos7 , Ceph V12 openstack pike 与 ceph 集成 http://www.cnblogs.com/elvi/p/7897 ...
- icheck如何修改样式大小
icheck默认样式比较大,有需求调整checkbox大小. 1. 最简单的方法可用css3来实现,让整个模块层缩放,不兼容低版本浏览器. transform:scale(0.7,0.7) 2. ...
- 缓存与ABP Redis Cache
缓存与ABP Redis Cache 为什么要用缓存 为什么要用缓存呢,说缓存之前先说使用缓存的优点. 减少寄宿服务器的往返调用(round-trips). 如果缓存在客户端或是代理,将减少对服务器的 ...
- iOS生成Bundle包及使用
什么是Bundle文件? 简单理解,就是资源文件包.我们将许多图片.XIB.文本文件组织在一起,打包成一个Bundle文件.方便在其他项目中引用包内的资源. Bundle文件的特点? Bundle是静 ...
- 【JDK1.8】JDK1.8集合源码阅读——IdentityHashMap
一.前言 今天我们来看一下本次集合源码阅读里的最后一个Map--IdentityHashMap.这个Map之所以放在最后是因为它用到的情况最少,也相较于其他的map来说比较特殊.就笔者来说,到目前为止 ...
- JS 中对变量类型的五种判断方法
5种基本数据类型:undefined.null.boolean.unmber.string 复杂数据类型:object. object:array.function.date等 方法一:使用typeo ...
- Eclipse Maven Mybatis的使用
关于maven的使用网上有太多教程,这里就不再介绍.本篇文章只用来记录 在Eclipse中使用maven创建含有mybatis的程序的配置,及注意事项. 使用Eclipse创建Maven项目 创建后的 ...
- 剑指Offer_6_从尾到头打印链表
题目描述 输入应该链表的头节点 , 从尾到头反过来打印出每个节点的值.链表定义如下 : typedef struct ListNode { int m_nKey ; ListNode * ...
- 最全Pycharm教程(28)——Pycharm搜索导航之搜索应用实例
1.主题 这里我们将介绍Pycharm另外一项强力的搜索导航功能.如果你希望知道某个特定的类或方法都在project中的哪些地方发挥了作用.也就是找出其全部的usages,这将是一个很巨大而繁琐的pr ...