Go Error 的设计哲学


最近阅读了不少关于 Go 错误处理的材料和讨论,总结出 Go Error 的设计哲学主要有两点:

Go 对错误处理的最初设计和后续优化方案,以及编程过程中处理错误的各种方法,基本都是围绕着这些理念展开的。

1 处理所有潜在的错误

Go 从最初设计起就确定了一个原则:程序中的所有潜在的错误都必须被明确地处理。为什么要这么做?可以先看下面一段代码:

1
2
3
4
5
6
7
8
func CopyFile(src, dst string) throws error {
	r := os.Open(src)
	defer r.Close()

	w := os.Create(dst)
	io.Copy(w, r)
	w.Close()
}

在这段代码中, os.Open os.Create io.Copy等函数都可能失败引发错误,当错误发生时会抛出错误 throws error ,但我们并不能直接知道具体哪个函数出错,并依此做出相应的处理(例如,当 io.Copy 出错时,我们需要删除了 dst 文件)。

这种 throws error 这种处理方式,和很多其它编程语言里采用的 try catch 类似,都属于隐晦地处理错误的方法。而 Go 希望避免这种情况,它希望清楚地处理错误(error),而不是把它当作异常(exception) 。按照这个原则,代码应该如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func CopyFile(src, dst string) error {
	r, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	defer r.Close()

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

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

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

这段代码基本上对潜在的错误都做了检查和处理,使程序更加健壮。

Go 对错误处理经常会提到两句话:

Allways check your errors!

Do not treat errors like exception

其实就是在实践这一设计理念。

不过,目前这种对错误的处理方法并不是完美的,代码中重复出现很多次 if err != nil ,可读性较差,这也是 Go 社区近年来试图解决的一个问题。

Go2 的错误处理讨论草案曾提出引入 check / handle 关键字,内置函数 try() 等新特性来避免重复的错误检查,因为担心这些方案不能带来显著的改善,目前还没有进一步的计划。

2 Errors Are Values

“Errors Are Values”,是 Go Error 设计的第二个哲学,它的主要内涵是 error 是可以被赋值,即通过代码来自定义 error 。

在 Go 语言里,error 是一个接口类型,只要实现了 Error() 方法,就可以自定义一个错误结构,这是 Errors Are Values 的基础。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type error interface {
    Error() string
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

这样的设计,使得我们在处理错误时有很大的发挥空间。

以上述备受争议 if err != nil 为例,虽然很多人诟病这个语句会在代码中出现太多次,可读性很差。但 Rob Pike 就认为,这在很多情况下是可以避免的,很多人只是简单地了解了 Go 处理错误的范式,没有更深入地去领会它,每次处理错误时就打上 if err != nil ,才会重复很多次这个语句。如果真正领会了 “Errors Are Values” 的含义,就可以通过代码来改变这一情况。

以下面代码为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
_, 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
}
// and so on

if err != nil 重复出现了多次,十分累赘,但我们可以根据这个场景自定义一个错误结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
}

上述的代码就可以改成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

经过调整后的代码更加整洁,同时也能清楚地处理错误。这种处理错误的方式,在官方标准库中也有不少应用,例如 bufio 的 Write 就使用了类似方法。Go 官方博客介绍错误处理的文章 里,“Simplifying repetitive error handling“ 一节也介绍相似的处理方法。

Rob Pike 也强调,这种对错误的处理方式并非唯一的对策,Errors Are Values 的设计,给我们带来了更多可能性。

可以预见,未来 Go 对错误的更新迭代,仍然离不开这两点设计哲学。假如 Go 的错误处理未来不能变得足够好,那也可能跟这两点设计哲学。

参考资料

【 Error handling and Go 】Go 错误的基础知识和使用介绍

【Working with Errors in Go 1.13 】Go 1.13 带来的新特性 Uwarp , errors.Is ,errors.As

【errors-are-values】 Rob Pike 介绍 errors are values 的应用

【error handling problem overview】Russ Cox 总结错误处理存在的问题

【https://blog.golang.org/experiment#Errors】 Russ Cox 介绍错误处理的改进进展

【Proposal: A built-in Go error check function, “try”】“try” 提案讨论

【Go 2 Draft Designs】Go 2 设计草案,关于错误部分占了不少,有的已经应用到 Go 1.13