优化 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
代码块的根本目的是让错误检查和处理减少,不要干扰正常业务代码,让正常业务代码更具有视觉连续性。大概有两个方向:
- 改善代码的视觉呈现
- 降低
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
检查的减少而降低。