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 来完成错误处理。提醒一下,此方案的缺点是要到最后才能知道有没有错误,好在如此的控制粒度在多数时候并无大碍。

总结

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

(0)

相关推荐

  • Golang报“import cycle not allowed”错误的2种解决方法

    前言 相信不少 Gopher 在写 Golang 程序都遇到过 import cycle not allowed 问题,本人最近研读 go-ethereum 源码时,发现定义 interface 也能解决此问题, 还能解决连分包都不能解决的情况, 并且比分包更加简单快捷.下面逐个讲解 分包 和 定义接口 这两种方法. 1. 应用场景 假设有如下使用场景: A 是应用程序的框架级结构体,在 A 包含子模块 B 和 C 的指针: B 为了方便的使用应用的其他子模块(比如 C )功能,所以在其结构体包

  • golang新手们容易犯的3个错误总结

    前言 从golang小白到成为golang工程师快两个月了,我要分享一下新手在开发中常犯的错误,都是我亲自踩过的坑.这些错误中有些会导致无法通过编译,这种错容易发现,而有些错误在编译时不会抛出,甚至在运行时也不会panic,如果缺少相关的知识,挠破头皮都搞不清楚bug出在哪. 1.对nil map.nil slice 添加数据 请考虑一下这段代码是否有错,然后运行一遍: package main func main() { var m map[string]string m["name"

  • golang log4go的日志输出优化详解

    前言 在go语言中,自身已经集成了一定log模块,开发者可以使用go语言自身的log包(import "log") .也有不少对自身log的开源封装.对于一些简单的开发,自身的log模块就已经足够应付.但是对一些大型,复杂的开发,log需要分门别类的输出,或者通过网络进行输出,自身log模块将难以应对. 当前也有一些比较重量级的log模块,比如logrus,可以实现比较复杂的功能.这里介绍一个轻量级的log模块--log4go 最近又看了一些golang的日志包和相关的文章,仔细阅读了

  • Golang常见错误之值拷贝和for循环中的单一变量详解

    前言 golang(中文名:go语言)是谷歌2009发布的第二款开源编程语言.Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全.支持并行进程..如果你想知道得更多,请移步至官网golang官网 在 Go 中函数的调用是值拷贝 copy value,而且在 for 循环中 v 的变量始终是一个变量.如果 v 是 pointer,print 这个 method 接收的是指针的拷贝,for 循环体中每次迭代 v 的 pointer va

  • Golang巧用defer进行错误处理的方法

    本文主要跟大家介绍了Golang巧用defer进行错误处理的相关内容,分享出来供大家参考学习,下面来看看详细的介绍: 问题引入 毫无疑问,错误处理是程序的重要组成部分,有效且优雅的处理错误是大多数程序员的追求.很多程序员都有C/C++的编程背景,Golang的程序员也不例外,他们处理错误有意无意的带着C/C++的烙印. 我们看看下面的例子,就有一种似曾相识的赶脚,代码如下: func deferDemo() error { err := createResource1() if err != n

  • 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.Wr

  • Golang中的错误处理的示例详解

    目录 1.panic 2.包装错误 3.错误类型判断 4.错误值判断 1.panic 当我们执行panic的时候会结束下面的流程: package main import "fmt" func main() { fmt.Println("hello") panic("stop") fmt.Println("world") } 输出: go run 9.go hellopanic: stop 但是panic也是可以捕获的,我们可

  • Golang中的错误处理深入分析

    目录 一.Go的内建类型error 二.怎么判断一个错误值具体代表那一类错误 知道错误类型的所属范围 知道错误变量是哪几个值 三.错误值体系的两种方法 立体的-错误类型体系 扁平的-错误值列表 一.Go的内建类型error error类型其实是一个接口类型,也是GO语言的内建类型: 在这个接口类型的声明中只包含了一个方法Error: Error方法不接受任何参数,但是会返回一个string类型的结果. 可以通过errors.New(string) error 方法声明一个error类型的变量:

  • java统计字符串中重复字符出现次数的方法

    本文实例讲述了java统计字符串中重复字符出现次数的方法.分享给大家供大家参考,具体如下: package com; import org.junit.Test; /** * 统计一个字符串的重复字符出现的次数 * * @author zdw * */ public class StringTest { @Test public void test() { String s = "fdfaacceeeeeeeeeeeegghikkkkkoooo"; count(s); } public

  • python获取指定字符串中重复模式最高的字符串方法

    给定一个字符串,如何得到其中重复模式最高的子字符串,我采用的方法是使用滑窗机制,对给定的字符串切分,窗口的大小从1增加到字符串长度减1,将所有的得到的切片统计结果,在这里不考虑单个字符的重复模式,好了,很简单看具体实现: #!usr/binenv python #encoding:utf-8 ''' __Author__:沂水寒城 统计一个给定字符串中重复模式数量得到最高重复模式串 ''' def slice(num_str,w): ''' 对输入的字符串滑窗切片返回结果列表 ''' resul

  • Python实现删除排序数组中重复项的两种方法示例

    本文实例讲述了Python实现删除排序数组中重复项的两种方法.分享给大家供大家参考,具体如下: 对于给定的有序数组nums,移除数组中存在的重复数字,确保每个数字只出现一次并返回新数组的长度 注意:不能为新数组申请额外的空间,只允许申请O(1)的额外空间修改输入数组 Example 1: Given nums = [1,1,2], Your function should return length = 2, with the first two elements of nums being 1

  • IOS开发中加载大量网络图片优化方法

    IOS开发中加载大量网络图片如何优化 1.概述 在IOS下通过URL读一张网络图片并不像其他编程语言那样可以直接把图片路径放到图片路径的位置就ok,而是需要我们通过一段类似流的方式去加载网络图片,接着才能把图片放入图片路径显示.比如: -(UIImage *) getImageFromURL:(NSString *)fileURL { //NSLog(@"执行图片下载函数"); UIImage * result; NSData * data = [NSData dataWithCont

  • C语言 数组中重复的数字分析及方法

    C语言 数组中重复的数字解决方法: 题目:在一个长度为n的数组里的所有数字都在0-n-1的 范围内.数组中某些数字是重复的,但是不知道有几个数字重复了,也不知道每个数字重复了几次.请找出数组中任意一个重复的数字.例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3. 解法1:对于数组进行排序,之后对于已经排序的数组进行遍历便可知道数组中重复的数字. 时间复杂度;O(nlogn); 解法2:建立一个大小为O(N)的哈希表,遍历数组中的元素并判断是否存在于哈

  • Python中ImportError错误的详细解决方法

    目录 ImportError: No module named XXX 错误解决 问题 解决 补充:ImportError: libGL.so.1: cannot open shared object file: No such file or directory 报错信息 报错分析 解决方案 总结 ImportError: No module named XXX 错误解决 问题 错误:ImportError: No module named XXX 在Python的工程中,偶尔会遇到文件无法引

  • golang中使用匿名结构体的方法

    目录 转化为map 定义具名结构体 定义匿名结构体 在一些项目中, 我们会使用json 来将字符串转为结构体,但是很多时候,这种结构体只会用一次,基本上只会用于反序列化, 对于这种只用到一次的结构体, 我们可以使用匿名结构体. 在gin 接收参数的时候会非常有用, 如我们将要接收的json 参数为 { "opt": "left", "phoneinfo": [ {"device_id": 64, "sn":

随机推荐