Loading

Go语言精进之路读书笔记第38条——尽量优化反复出现的if err != nil

Go在最初设计时就有意识地选择了使用显式错误结果和显式错误检查

38.1 两种观点

显式的错误处理方式让Go程序员首先考虑失败情况,这将引导Go程序员在编写代码时处理故障,而不是在程序部署并运行在生产环境后再处理。而为反复出现的代码片段if err != nil {...}所付出的成本已基本被在故障发生时处理故障的成本超过。

try提案:与Go完成一件事情仅有一种方法的原则相背离,有的人使用新引入的try,有的人则依旧喜欢使用传统的if err != nil的错误检查。

38.2 尽量优化

可以通过良好的设计减少或消除这类反复出现的错误检查。

38.3 优化思路

(1) 改善代码的视觉呈现

try提案(已经被否决)

(2) 降低if err != nil 重复的次数

利用圈复杂度(Cyclomatic complexity)来进行衡量

1.视觉扁平化

将触发错误处理的语句与错误处理代码放在一行

if _, err = io.Copy(w, r); err != nil {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

2.重构:减少if err != nil的重复次数

增加中间层,将大函数拆分成小函数,降低圈复杂度,拆分后每个函数的if err != nil重复次数都在可接受范围内

func openBoth(src, dst string) (*os.File, *os.File, error) {
    var r, w *os.File
    var err error
    if r, err = os.Open(src); err != nil {
        return nil, nil, fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if w, err = os.Create(dst); err != nil {
        r.Close()
        return nil, nil, fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    return r, w, nil
}

func CopyFile(src, dst string) error {
    var err error
    var r, w *os.File
    if r, w, err = openBoth(src, dst); err != nil {
        return err
    }
    defer func() {
        r.Close()
        w.Close()
        if err != nil {
            os.Remove(dst)
        }
    }()

    if _, err = io.Copy(w, r); err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    return nil
}

func main() {
    err := CopyFile("foo.txt", "bar.txt")
    if err != nil {
        fmt.Println("copyfile error:", err)
        return
    }
    fmt.Println("copyfile ok")
}

3.check / handle 风格化(不推荐)

业务逻辑中不建议使用panic,panic和recover让函数调用的性能下降了约90%

4.封装:内置error状态 (推荐)

“Errors are values”一文中,Rob Pike为我们呈现了在Go标准库中使用了避免if err != nil反复出现的一种代码设计思路。

bufio包的Writer就是使用这个思路实现的,因此它可以像下面这样使用:

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的返回错误值,因为错误状态被封装在bufio.Writer结构的内部了,Writer定义了一个err字段作为内部错误状态值,它与Writer的实例绑定在了一起,并且在Write方法的入口判断是否为nil。一旦不为nil,Write什么都不做就会返回

// $GOROOT/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) {
    for len(p) > b.Available() && b.err == nil {
        ...
    }
    if b.err != nil {
        return nn, b.err
    }
    ......
    return nn, nil
}

参考bufio包的思路,优化之前的代码

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
    }

    if _, err := io.Copy(f.w, f.r); 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 {
        fmt.Println("copy file error:", err)
        return
    }
    fmt.Println("copy file ok")
}
posted @ 2024-02-28 21:04  brynchen  阅读(127)  评论(0)    收藏  举报