0%

【译】Go2设计草案

作为Go2设计过程的一部分,我们已经发布了以下三个主题的设计草案用于开启社区的讨论,它们分别是:泛型(generics)、错误处理(error handling)和错误值语义(error value semantics)。

从Go提案流程的意义上说,这些设计草案不是提案。它们是讨论的起点,最终的目标是产生足以被转化为实际建议的设计。

每个设计草案都附有“问题概述”。问题概述旨在提供问题的上下文;为实际的设计文档打下基础,其中当然会提供设计细节;并帮助设计和指导有关设计的讨论。它包含了背景、目标、非目标、设计约束、设计摘要、我们认为最需要关注的领域的简短讨论以及与以前方法的比较。

再次强调,这些是设计草案,而不是最终的方案。我们希望所有Go用户都能帮助我们改进他们并将其转变为Go提案。我们建立了一个Wiki页面来收集和组织有关每个Topic的反馈。请帮助我们使这些页面保持最新状态,包括添加指向您自己的反馈的链接。

错误处理(Error handling)

介绍

Go2的总体目标是努力找到方法解决Go无法扩展到大型代码库和大型开发者的问题。

编写错误检查和错误处理代码是Go程序无法很好扩展的一个原因。通常,Go程序有太多的错误检查代码,而没有足够的代码来处理它们。(这将在下面说明)设计草案旨在通过引入比当前惯用的语句组合更轻量的错误检查语法来解决此问题。

作为Go 2的一部分,我们还单独考虑更改错误值的语义。

问题

为了扩展到较大的代码库,Go程序必须是轻量级的,没有不必要的重复,同时必须健壮的,可以在出现错误时优雅地对其进行处理。

在Go的设计中,我们做出了有意识的选择,以使用显式错误结果和显式错误检查。相比之下,C通常显式的对隐式的错误结果以及errorno进行检查,而异常处理(包括C ++,C#,Java和Python在内的许多语言中都有)相当于隐式结果的隐式检查。

在Raymond Chen的两对博客文章中,对隐式检查的微妙性有很好的总结:““Cleaner, more elegant, and wrong”(2004年)和““Cleaner, more elegant, and harder to recognize”(2005年)。从本质上讲,因为您根本看不到隐式检查,所以很难检查验证错误处理代码在检查失败时是否可以将程序状态从错误中恢复。

例如,考虑以下代码,该代码使用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()
}

这是一个美好,整洁,优雅的代码。但它的错误也是不可见的:如果io.Copy还是w.Close失败,代码不会删除被部分写入的dst文件。

另一方面,在实际中Go代码的呈现会如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
defer w.Close()

if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
}

这段代码不够好,不整洁,不优雅,并且仍然存在错误:与以前的版本一样,它不会在出现io.Copy或w.Close失败时将dst文件删除。有一个合理的论点,即可见的错误检查会使得细心的代码阅读者了解正确的错误处理响应。但是在实践中,错误检查占用了很大的空间,以至于读者需要快婿学会跳过它们以查看代码的结构。

该代码在错误处理方面还有第二个遗漏。函数通常应在错误中包含有关其参数的相关信息,例如os.Open返回正在打开的文件的名称。未经修改就返回错误中没有包含任何导致错误的操作顺序信息。

简而言之,上述的Go代码具有过多的错误检查和不足的错误处理。而更加健壮的版本如下:

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 != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}

但纠正这些错误只会使代码更正确,而不会使代码更清晰或更优雅。

目标

对于Go 2,我们希望错误检查可以更加轻量,从而减少专用于错误检查的Go程序文本数量。同时,我们还希望编写错误处理可以更加方便,从而提高程序员花费时间进行处理的可能性。

错误检查和错误处理都必须保持显式,即在程序文本中可见。我们不想重复异常处理的陷阱。

要保证现有的代码的有效性。任何更改都必须与现有代码互相兼容。

如上所述,更改或增强错误的语义不是设计草案的目标。

设计草案

本节快速总结了设计草案,将其作为高水准讨论以及和与其他方法进行比较的基础。

设计草案引入了两种新的语法形式。首先,它引入一个检查表达式 check f(x, y, z) 或者 check err*,标记一个显式的错误检查。其次,它引入了 *handle 语句用于定义错误处理程序。当错误检查失败时,它将控制权转移到最内层的错误处理程序,之后控制程序将控制权转移到更上层的处理程序,依此类推,直到处理程序执行一条return语句。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

r := check os.Open(src)
defer r.Close()

w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}

check io.Copy(w, r)
check w.Close()
return nil
}

使用 check/handle 组合允许函数本身不返回错误。例如,这是一个main函数的例子来自 useful but trivial program

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
hex, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}

data, err := parseHexdump(string(hex))
if err != nil {
log.Fatal(err)
}

os.Stdout.Write(data)
}

写起来会更短更清晰:

1
2
3
4
5
6
7
8
9
func main() {
handle err {
log.Fatal(err)
}

hex := check ioutil.ReadAll(os.Stdin)
data := check parseHexdump(string(hex))
os.Stdout.Write(data)
}

更详细的信息,请参阅草稿设计

讨论和开放性问题

这些设计草案仅作为社区讨论的起点。我们非常希望可以根据反馈(尤其是经验报告)对细节进行修订。本节概述了仍有待回答的一些问题。

check还是try。关键字check明确说明了正在执行的操作。最初,我们使用众所周知的异常处理关键字try。对于函数调用,它的阅读性确实很不错:

1
data := try parseHexdump(string(hex))

但是对于需要对错误值进行校验的情况,它的读取效果并不理想:

1
2
3
4
5
data, err := parseHexdump(string(hex))
if err == ErrBadHex {
... special handling ...
}
try err

在这种情况下,check err比起try err更加清晰。Rust起初使用于 try! 标记一个显式的错误检查,但后来改为使用特殊的 ? 运算符。Swift不仅使用 try 标记显式错误检查,而且还用于 try!try? ,同时,和大多数的错误处理一样,throwcatch也是错误处理的一部分。

总体而言,设计草案中的 check/handle 与Rust和Swift中的异常处理是十分不同的,它使用更清晰的关键字check,而不是更熟悉的关键字try

Defer 错误处理程序在某些方面类似于defer和recover,但是只针对errors而不是panics。当前的草案设计使错误处理程序按词法构建执行链,而defer根据代码的执行在运行时构建执行链条。这种差异对于在条件主体和循环中声明的处理程序(或延迟函数)很重要。尽管错误处理程序使用词法栈看起来似乎是一个略微更好的设计,但defer准确匹配可能并不奇怪。作为一个类似的defer处理会更方便的示例,如果CopyFile将其目的地设置w为os.Stdout或结果os.Create,则能够os.Remove(dst)有条件地引入处理程序将很有帮助。

Panics 我们花了一段时间来尝试统一error handle和panic,使得我们可以在已经处理error的情况不必额外处理panic。但我们所有对统一的尝试都带来了更多的复杂性。

Feedback 最有用的一般反馈将是设计草案启用或不允许的有趣用途的示例。我们也欢迎您对以上几点提出反馈,尤其是根据实际程序中复杂或错误的错误处理经验。

我们正在https://golang.org/wiki/Go2ErrorHandlingFeedback上收集反馈的链接。

欢迎关注我的其它发布渠道