mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-04 15:01:46 +00:00
转为 mdbook
This commit is contained in:
155
ch5/ch5-04.md
155
ch5/ch5-04.md
@@ -34,6 +34,157 @@ Go这样设计的原因是由于对于某个应该在控制流程中处理的错
|
||||
|
||||
正因此,Go使用控制流机制(如if和return)处理错误,这使得编码人员能更多的关注错误处理。
|
||||
|
||||
{% include "./ch5-04-1.md" %}
|
||||
### 5.4.1. 错误处理策略
|
||||
|
||||
{% include "./ch5-04-2.md" %}
|
||||
当一次函数调用返回错误时,调用者应该选择合适的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。
|
||||
|
||||
首先,也是最常用的方式是传播错误。这意味着函数中某个子程序的失败,会变成该函数的失败。下面,我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败,findLinks会直接将这个HTTP错误返回给调用者:
|
||||
|
||||
```Go
|
||||
resp, err := http.Get(url)
|
||||
if err != nil{
|
||||
return nil, err
|
||||
}
|
||||
```
|
||||
|
||||
当对html.Parse的调用失败时,findLinks不会直接返回html.Parse的错误,因为缺少两条重要信息:1、发生错误时的解析器(html parser);2、发生错误的url。因此,findLinks构造了一个新的错误信息,既包含了这两项,也包括了底层的解析出错的信息。
|
||||
|
||||
```Go
|
||||
doc, err := html.Parse(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
|
||||
}
|
||||
```
|
||||
|
||||
fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回。我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链,就像美国宇航局事故调查时做的那样:
|
||||
|
||||
```
|
||||
genesis: crashed: no parachute: G-switch failed: bad relay orientation
|
||||
```
|
||||
|
||||
由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。最终的错误信息可能很长,我们可以通过类似grep的工具处理错误信息(译者注:grep是一种文本搜索工具)。
|
||||
|
||||
编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。
|
||||
|
||||
以os包为例,os包确保文件操作(如os.Open、Read、Write、Close)返回的每个错误的描述不仅仅包含错误的原因(如无权限,文件目录不存在)也包含文件名,这样调用者在构造新的错误信息时无需再添加这些信息。
|
||||
|
||||
一般而言,被调用函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息,比如添加url到html.Parse返回的错误中。
|
||||
|
||||
让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
|
||||
|
||||
<u><i>gopl.io/ch5/wait</i></u>
|
||||
```Go
|
||||
// WaitForServer attempts to contact the server of a URL.
|
||||
// It tries for one minute using exponential back-off.
|
||||
// It reports an error if all attempts fail.
|
||||
func WaitForServer(url string) error {
|
||||
const timeout = 1 * time.Minute
|
||||
deadline := time.Now().Add(timeout)
|
||||
for tries := 0; time.Now().Before(deadline); tries++ {
|
||||
_, err := http.Head(url)
|
||||
if err == nil {
|
||||
return nil // success
|
||||
}
|
||||
log.Printf("server not responding (%s);retrying…", err)
|
||||
time.Sleep(time.Second << uint(tries)) // exponential back-off
|
||||
}
|
||||
return fmt.Errorf("server %s failed to respond after %s", url, timeout)
|
||||
}
|
||||
```
|
||||
|
||||
如果错误发生后,程序无法继续运行,我们就可以采用第三种策略:输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
|
||||
|
||||
```Go
|
||||
// (In function main.)
|
||||
if err := WaitForServer(url); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
```
|
||||
|
||||
调用log.Fatalf可以更简洁的代码达到与上文相同的效果。log中的所有函数,都默认会在错误信息之前输出时间信息。
|
||||
|
||||
```Go
|
||||
if err := WaitForServer(url); err != nil {
|
||||
log.Fatalf("Site is down: %v\n", err)
|
||||
}
|
||||
```
|
||||
|
||||
长时间运行的服务器常采用默认的时间格式,而交互式工具很少采用包含如此多信息的格式。
|
||||
|
||||
```
|
||||
2006/01/02 15:04:05 Site is down: no such domain:
|
||||
bad.gopl.io
|
||||
```
|
||||
|
||||
我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。
|
||||
|
||||
```Go
|
||||
log.SetPrefix("wait: ")
|
||||
log.SetFlags(0)
|
||||
```
|
||||
|
||||
第四种策略:有时,我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数
|
||||
|
||||
```Go
|
||||
if err := Ping(); err != nil {
|
||||
log.Printf("ping failed: %v; networking disabled",err)
|
||||
}
|
||||
```
|
||||
|
||||
或者标准错误流输出错误信息。
|
||||
|
||||
```Go
|
||||
if err := Ping(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
|
||||
}
|
||||
```
|
||||
|
||||
log包中的所有函数会为没有换行符的字符串增加换行符。
|
||||
|
||||
第五种,也是最后一种策略:我们可以直接忽略掉错误。
|
||||
|
||||
```Go
|
||||
dir, err := ioutil.TempDir("", "scratch")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %v",err)
|
||||
}
|
||||
// ...use temp dir…
|
||||
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically
|
||||
```
|
||||
|
||||
尽管os.RemoveAll会失败,但上面的例子并没有做错误处理。这是因为操作系统会定期的清理临时目录。正因如此,虽然程序没有处理错误,但程序的逻辑不会因此受到影响。我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。
|
||||
|
||||
在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。
|
||||
|
||||
### 5.4.2. 文件结尾错误(EOF)
|
||||
|
||||
函数经常会返回多种错误,这对终端用户来说可能会很有趣,但对程序而言,这使得情况变得复杂。很多时候,程序必须根据错误类型,作出不同的响应。让我们考虑这样一个例子:从文件中读取n个字节。如果n等于文件的长度,读取过程的任何错误都表示失败。如果n小于文件的长度,调用者会重复的读取固定大小的数据直到文件结束。这会导致调用者必须分别处理由文件结束引起的各种错误。基于这样的原因,io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:
|
||||
|
||||
```Go
|
||||
package io
|
||||
|
||||
import "errors"
|
||||
|
||||
// EOF is the error returned by Read when no more input is available.
|
||||
var EOF = errors.New("EOF")
|
||||
```
|
||||
|
||||
调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。(4.3的chartcount程序展示了更加复杂的代码)
|
||||
|
||||
```Go
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
r, _, err := in.ReadRune()
|
||||
if err == io.EOF {
|
||||
break // finished reading
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read failed:%v", err)
|
||||
}
|
||||
// ...use r…
|
||||
}
|
||||
```
|
||||
|
||||
因为文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息。在7.11节中,我们会提出更系统的方法区分某些固定的错误值。
|
||||
|
@@ -244,4 +244,61 @@ http://research.swtch.com/gotour
|
||||
|
||||
**练习5.14:** 使用breadthFirst遍历其他数据结构。比如,topoSort例子中的课程依赖关系(有向图)、个人计算机的文件层次结构(树);你所在城市的公交或地铁线路(无向图)。
|
||||
|
||||
{% include "./ch5-06-1.md" %}
|
||||
### 5.6.1. 警告:捕获迭代变量
|
||||
|
||||
本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。
|
||||
|
||||
考虑这样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。
|
||||
|
||||
```Go
|
||||
var rmdirs []func()
|
||||
for _, d := range tempDirs() {
|
||||
dir := d // NOTE: necessary!
|
||||
os.MkdirAll(dir, 0755) // creates parent directories too
|
||||
rmdirs = append(rmdirs, func() {
|
||||
os.RemoveAll(dir)
|
||||
})
|
||||
}
|
||||
// ...do some work…
|
||||
for _, rmdir := range rmdirs {
|
||||
rmdir() // clean up
|
||||
}
|
||||
```
|
||||
|
||||
你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。
|
||||
|
||||
```go
|
||||
var rmdirs []func()
|
||||
for _, dir := range tempDirs() {
|
||||
os.MkdirAll(dir, 0755)
|
||||
rmdirs = append(rmdirs, func() {
|
||||
os.RemoveAll(dir) // NOTE: incorrect!
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。
|
||||
|
||||
通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。
|
||||
|
||||
```Go
|
||||
for _, dir := range tempDirs() {
|
||||
dir := dir // declares inner dir, initialized to outer dir
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
这个问题不仅存在基于range的循环,在下面的例子中,对循环变量i的使用也存在同样的问题:
|
||||
|
||||
```Go
|
||||
var rmdirs []func()
|
||||
dirs := tempDirs()
|
||||
for i := 0; i < len(dirs); i++ {
|
||||
os.MkdirAll(dirs[i], 0755) // OK
|
||||
rmdirs = append(rmdirs, func() {
|
||||
os.RemoveAll(dirs[i]) // NOTE: incorrect!
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
如果你使用go语句(第八章)或者defer语句(5.8节)会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# 第五章 函数
|
||||
# 第5章 函数
|
||||
|
||||
函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。函数的机制可以让我们将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。一个函数同时对用户隐藏了其实现细节。由于这些因素,对于任何编程语言来说,函数都是一个至关重要的部分。
|
||||
|
||||
|
Reference in New Issue
Block a user