如何优化Golang中重复的错误处理

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

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

_, 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,而是在方法内部判断一旦有问题就马上返回,有了这些准备工做,咱们就能够把本来穿插在业务逻辑中间的错误判断提出来放到最后来统一调用,从而在视觉上保证让人能够直观的看出代码原本的意图是什么。ui

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

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 操做都不得不进行重复的错误处理,看上去是坏味道,改进版:code

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 便可。token

相似的作法在 Golang 标准库中家常便饭,让咱们继续看看 Eliminate error handling by eliminating errors 中提到的一个关于 bufio.Reader 和 bufio.Scanner 的例子:string

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」,看上去这是坏味道,改进版:it

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 的定义:io

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


原文连接:https://huoding.com/2019/04/11/728

相关文章
相关标签/搜索