Go 语言
错误处理
优化 if err!= nil

优化 if err != nil

Go 在错误处理方面体现出的这种与主流语言格格不入,让很多来自这些主流语言的 Go 初学者感到困惑:Go 代码中反复出现了太多方法单一的错误检查 if err != nil。比如:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return err
    }
    defer r.Close()
 
    w, err := os.Create(dst)
    if err != nil {
        return err
    }
 
    _, err = io.Copy(w, r)
    if err != nil {
        w.Close()
        os.Remove(dst)
        return err
    }
 
    err := w.Close()
    if err != nil {
        os.Remove(dst)
        return nil
    }
}

对于 Go 错误处理方式再某些时候显得过于冗长甚至啰嗦。但社区和专家们对其的看法出现了分歧。

在未引入 try 或 check/handle 这些新语法的情况下,怎么优化呢?

优化思路

优化反复出现的 if err != nil 代码块的根本目的是让错误检查和处理减少,不要干扰正常业务代码,让正常业务代码更具有视觉连续性。大概有两个方向:

  1. 改善代码的视觉呈现
  2. 降低 if err != nil 出现次数

内置 error 状态

我们来看一个例子:

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
 
if b.Flush() != nil {
    return b.Flush()
}

上述代码中并没有判断三个 b.Write 的返回错误值,那么错误放在哪?

我们打开 src/bufio/bufio.go 文件,可以看到下面的代码:

type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}
 
func (b *Writer) Write(p []byte) (nn int, err error) {
    ...
    if b.err != nil {
        return nn, b.err
    }
    ...
    return nn, nil
}

可以看到,错误状态被封装在 bufio.Writer 结构体中,Writer 定义了一个 err 字段作为内部错误状态值,它与 Writer 的实例绑定在一起, 并且在 Write 方法的入口判断是否为 nil,如果不为 nil,就什么都不做,直接返回。

这显然是消除 if err != nil 的理想方法。

我们以 CopyFile 为例,进行改造:

type FileCopier struct {
    w *os.File
    r *os.File
    err error
}
 
func (f *FileCopier) Open(path string) (*os.File, error) {
    if f.err != nil {
        return nil, f.err
    }
 
    h, err := os.Open(path)
    if err != nil {
        f.err = err
        return nil, err
    }
    return h, nil
}
 
func (f *FileCopier) OpenSrc(path string) {
    if f.err != nil {
        return
    }
 
    f.r, f.err = f.Open(path)
    return
}
 
func (f *FileCopier) CreateDst(path string) {
    if f.err != nil {
        return
    }
 
    f.w, f.err = os.Create(path)
    return
}
 
func (f *FileCopier) Copy() {
    if f.err != nil {
        return
    }
 
    _, err = io.Copy(f.w, f.r)
    if err != nil {
        f.err = err
    }
}
 
func (f *FileCopier) CopyFile(src, dst string) error {
    if f.err != nil {
        return f.err
    }
 
    defer func() {
        if f.r != nil {
            f.r.Close()
        }
        if f.w != nil {
            f.w.Close()
        }
        if f.err != nil {
            if f.w != nil {
                os.Remove(dst)
            }
        }
    }()
 
    f.OpenSrc(src)
    f.CreateDst(dst)
    f.Copy()
    return f.err
}
 
func main() {
    var fc FileCopier
    err := fc.CopyFile("foo.txt", "bar.txt")
    if err != nil {
        println(err)
    }
    println("copy file ok")
}

这次重构很彻底,我们把错误状态封装在了 FileCopier 结构体中,FileCopier 结构体的方法中,只要有一个方法返回错误,后续的方法就不会执行了。

这样,这样我们只需要按照正常的业务逻辑,顺序执行 OpenSrc -> CreateDst -> Copy,不需要再关心错误处理。 正常的业务逻辑的视觉连续性就很好地实现了。同时该 CopyFile 方法的复杂度因 if 检查的减少而降低。