Golang中重复错误处理的优化方法

 更新时间:2019年04月12日 09:54:31   作者:老王  
这篇文章主要给大家介绍了关于Golang中重复错误处理优化的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Golang具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

Golang 错误处理最让人头疼的问题就是代码里充斥着「if err != nil」,它们破坏了代码的可读性,本文收集了几个例子,让大家明白如何优化此类问题。

让我们看看 Errors are values 中提到的一个 io.Writer 例子:

_, 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
}

如上代码乍一看无法直观的看出其本来的意图是什么,改进版:

type errWriter struct {
 w io.Writer
 err error
}

func (ew *errWriter) write(buf []byte) {
 if ew.err != nil {
 return
 }
 _, ew.err = ew.w.Write(buf)
}

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
if ew.err != nil {
 return ew.err
}

通过自定义类型 errWriter 来封装 io.Writer,并且封装了 error,新类型有一个 write 方法,不过其方法签名并没有返回 error,而是在方法内部判断一旦有问题就立刻返回,有了这些准备工作,我们就可以把原本穿插在业务逻辑中间的错误判断提出来放到最后来统一调用,从而在视觉上保证让人可以直观的看出代码本来的意图是什么。

让我们再看看 Eliminate error handling by eliminating errors 中提到的另一个 io.Writer 例子:

type Header struct {
 Key, Value string
}

type Status struct {
 Code int
 Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
 _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
 if err != nil {
 return err
 }

 for _, h := range headers {
 _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
 if err != nil {
 return err
 }
 }

 if _, err := fmt.Fprint(w, "\r\n"); err != nil {
 return err
 }

 _, err = io.Copy(w, body)
 return err
}

第一感觉既然错误是 fmt.Fprint 和 io.Copy 返回的,是不是我们要重新封装一下它们?实际上真正的源头是它们的参数 io.Writer,因为直接调用 io.Writer 的 Writer 方法的话,方法签名中有返回值 error,所以每一步 fmt.Fprint 和 io.Copy 操作都不得不进行重复的错误处理,看上去是坏味道,改进版:

type errWriter struct {
 io.Writer
 err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
 if e.err != nil {
 return 0, e.err
 }

 var n int
 n, e.err = e.Writer.Write(buf)
 return n, nil
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
 ew := &errWriter{Writer: w}
 fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

 for _, h := range headers {
 fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
 }

 fmt.Fprint(ew, "\r\n")
 io.Copy(ew, body)

 return ew.err
}

通过自定义类型 errWriter 来封装 io.Writer,并且封装了 error,同时重写了 Writer 方法,虽然方法签名中仍然有返回值 error,但是我们单独保存了一份 error,并且在方法内部判断一旦有问题就立刻返回,有了这些准备工作,新版的 WriteResponse 不再有重复的错误判断,只需要在最后检查一下 error 即可。

类似的做法在 Golang 标准库中屡见不鲜,让我们继续看看 Eliminate error handling by eliminating errors 中提到的一个关于 bufio.Reader 和 bufio.Scanner 的例子:

func CountLines(r io.Reader) (int, error) {
 var (
 br = bufio.NewReader(r)
 lines int
 err error
 )

 for {
 _, err = br.ReadString('\n')
 lines++
 if err != nil {
 break
 }
 }

 if err != io.EOF {
 return 0, err
 }

 return lines, nil
}

我们构造一个 bufio.Reader,然后在一个循环中调用 ReadString 方法,如果读到文件结尾,那么 ReadString 会返回一个错误(io.EOF),为了判断此类情况,我们不得不在每次循环时判断「if err != nil」,看上去这是坏味道,改进版:

func CountLines(r io.Reader) (int, error) {
 sc := bufio.NewScanner(r)
 lines := 0

 for sc.Scan() {
 lines++
 }

 return lines, sc.Err()
}

实际上,和 bufio.Reader 相比,bufio.Scanner 是一个更高阶的类型,换句话简单点来说的话,相当于是 bufio.Scanner 抽象了 bufio.Reader,通过把低阶的 bufio.Reader 换成高阶的 bufio.Scanner,循环中不再需要判断「if err != nil」,因为 Scan 方法签名不再返回 error,而是返回 bool,当在循环里读到了文件结尾的时候,循环直接结束,如此一来,我们就可以统一在最后调用 Err 方法来判断成功还是失败,看看 Scanner 的定义:

type Scanner struct {
 r   io.Reader // The reader provided by the client.
 split  SplitFunc // The function to split the tokens.
 maxTokenSize int  // Maximum size of a token; modified by tests.
 token  []byte // Last token returned by split.
 buf   []byte // Buffer used as argument to split.
 start  int  // First non-processed byte in buf.
 end   int  // End of data in buf.
 err   error  // Sticky error.
 empties  int  // Count of successive empty tokens.
 scanCalled bool  // Scan has been called; buffer is in use.
 done   bool  // Scan has finished.
}

可见 Scanner 封装了 io.Reader,并且封装了 error,和我们之前讨论的做法一致。有一点说明一下,实际上查看 Scan 源代码的话,你会发现它不是通过 err 来判断是否结束的,而是通过 done 来判断是否结束,这是因为 Scan 只有遇到文件结束的错误才退出,其它错误会继续执行,当然,这只是具体的细节问题,不影响我们的结论。

通过对以上几个例子的分析,我们可以得出优化重复错误处理的大概套路:通过创建新的类型来封装原本干脏活累活的旧类型,同时在新类型中封装 error,新旧类型的方法签名可以保持兼容,也可以不兼容,这个不是关键的,视客观情况而定,至于具体的逻辑实现,先判断有没有 error,如果有就直接退出,如果没有就继续执行,并且在执行过程中保存可能出现的 error 以便后面操作使用,最后通过统一调用新类型的 error 来完成错误处理。提醒一下,此方案的缺点是要到最后才能知道有没有错误,好在如此的控制粒度在多数时候并无大碍。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

相关文章

  • go使用makefile脚本编译应用的方法小结

    go使用makefile脚本编译应用的方法小结

    makefile可以看作是make工具的脚本文件, 而make主要用来处理一系列命令。常用的比如用来编译和打包文件, 在C/C++的编译打包中应用最广泛了,这篇文章主要介绍了go使用makefile脚本编译应用,需要的朋友可以参考下
    2022-08-08
  • Go 实现 WebSockets之创建 WebSockets

    Go 实现 WebSockets之创建 WebSockets

    这篇文章主要介绍了Go 实现 WebSockets之创建 WebSockets,文章主要探索 WebSockets,并简要介绍了它们的工作原理,并仔细研究了全双工通信,想了解更多相关内容的小伙伴可以参考一下
    2022-04-04
  • Go语言对字符串进行MD5加密的方法

    Go语言对字符串进行MD5加密的方法

    这篇文章主要介绍了Go语言对字符串进行MD5加密的方法,实例分析了Go语言对字符串进行md5加密的技巧,需要的朋友可以参考下
    2015-03-03
  • goland中导包报红和go mod问题

    goland中导包报红和go mod问题

    这篇文章主要介绍了goland中导包报红和go mod问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • Go语言使用Etcd实现分布式锁

    Go语言使用Etcd实现分布式锁

    etcd是近几年比较火热的一个开源的、分布式的键值对数据存储系统,本文将介绍如何利用Etcd实现分布式锁,感兴趣的小伙伴可以跟随小编一起了解一下
    2023-05-05
  • 浅谈beego默认处理静态文件性能低下的问题

    浅谈beego默认处理静态文件性能低下的问题

    下面小编就为大家带来一篇浅谈beego默认处理静态文件性能低下的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Golang实现按行读取文件的方法小结

    Golang实现按行读取文件的方法小结

    按行读取文件相较于一次性载入,有着很多优势,如内存效率高、处理速度快、实时性高等,本文主要介绍了Golang按行读取文件的相关方法,希望对大家有所帮助
    2024-02-02
  • Go语言开源库实现Onvif协议客户端设备搜索

    Go语言开源库实现Onvif协议客户端设备搜索

    这篇文章主要为大家介绍了Go语言Onvif协议客户端设备搜索示例实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • 一文带你轻松理解Go中的内存逃逸问题

    一文带你轻松理解Go中的内存逃逸问题

    这篇文章主要给大家介绍Go中的内存逃逸问题,文中通过代码示例讲解的非常详细,对我们的学习或工作有一定的参考价值,感兴趣的同学可以跟着小编一起来学习
    2023-06-06
  • Go中的应用配置管理详解

    Go中的应用配置管理详解

    这篇文章主要为大家介绍了Go中的应用配置管理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09

最新评论