gopl-zh.github.com/ch5/ch5-04-1.md

5.9 KiB
Raw Blame History

5.4.1. 错误处理策略

当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况的不同,有很多处理方式,让我们来看看常用的五种方式。

首先也是最常用的方式是传播错误。这意味着函数中某个子程序的失败会变成该函数的失败。下面我们以5.3节的findLinks函数作为例子。如果findLinks对http.Get的调用失败findLinks会直接将这个HTTP错误返回给调用者

resp, err := http.Get(url)
if err != nil{
	return nill, err
}

当对html.Parse的调用失败时findLinks不会直接返回html.Parse的错误因为缺少两条重要信息1、错误发生在解析器2、url已经被解析。这些信息有助于错误的处理findLinks会构造新的错误信息返回给调用者

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返回的错误中。

让我们来看看处理错误的第二种策略。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

gopl.io/ch5/wait

// 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才能在库函数中结束程序。

// (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中的所有函数都默认会在错误信息之前输出时间信息。

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的前缀信息屏蔽时间信息一般而言前缀信息会被设置成命令名。

log.SetPrefix("wait: ")
log.SetFlags(0)

第四种策略有时我们只需要输出错误信息就足够了不需要中断程序的运行。我们可以通过log包提供函数

if err := Ping(); err != nil {
	log.Printf("ping failed: %v; networking disabled",err)
}

或者标准错误流输出错误信息。

if err := Ping(); err != nil {
	fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}

log包中的所有函数会为没有换行符的字符串增加换行符。

第五种,也是最后一种策略:我们可以直接忽略掉错误。

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中大部分函数的代码结构几乎相同首先是一系列的初始检查防止错误发生之后是函数的实际逻辑。