Library routines must often return some sort of error indication to the caller. As mentioned earlier, Go's multivalue return makes it easy to return a detailed error description alongside the normal return value. It is good style to use this feature to provide detailed error information. For example, as we'll see, os.Open doesn't just return a nil pointer on failure, it also returns an error value that describes what went wrong.
库例程必须总是返回某些类型的错误给调用者。如前所述,Go 拥有多返回值特性,这使得与普通返回值一道返回一个详细的错误描述变得容易。藉由此特性提供错误信息是一种很好的编码风格。如我们即将看到的例子,os.Open
函数在其失败时,不止返回一个空指针,同时还伴随一个错误值,用以描述错误发生的原因。
By convention, errors have type error, a simple built-in interface.
按照惯例,所有的 errors 都属于 error
类型,这是一个简单的内建接口。
type error interface {
Error() string
}
A library writer is free to implement this interface with a richer model under the covers, making it possible not only to see the error but also to provide some context. As mentioned, alongside the usual *os.File
return value, os.Open also returns an error value. If the file is opened successfully, the error will be nil
, but when there is a problem, it will hold an os.PathError
:
库作者在实现 error 接口时,可在表象之下自由地实现更加丰富的数据模型,这样接口能提供的信息就不局限于错误描述,还可以附加一些上下文信息。如前所述,除了正常返回 *os.File
以外,os.Open
还会返回错误值。如果文件打开成功,error 的值将会是 nil
,否则它将会持有一个 os.PathError
(译注:接口的动态类型
):
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
PathError
's Error generates a string like this:
PathError
的 Error 方法生成如下字符串:
open /etc/passwx: no such file or directory
Such an error, which includes the problematic file name, the operation, and the operating system error it triggered, is useful even if printed far from the call that caused it; it is much more informative than the plain "no such file or directory".
这样的 error 不仅包含了问题文件的名称,还包括相关的操作以及触发的系统错误,即使它被打印在远离其调用者的位置,也能为我们提供很多帮助;这样的错误远比未加修饰的 “no such file or directory” 信息量要大。
When feasible, error strings should identify their origin, such as by having a prefix naming the operation or package that generated the error. For example, in package image, the string representation for a decoding error due to an unknown format is "image: unknown format".
error 应尽可能标注其出处,比如冠以产生此错误的操作或者包的名称。例如,在 image 包中,因未知格式导致解码错误的信息为 “image: unknown format”。
Callers that care about the precise error details can use a type switch or a type assertion to look for specific errors and extract details. For PathErrors this might include examining the internal Err field for recoverable failures.
关注精确错误细节的调用者可使用 type switch 或者 type assertion 来寻找特殊的错误,并从中取得细节。对于 PathErrors
,可能需要检查内部的 Err
字段来为可恢复的失败做另行处理。
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
The second if statement here is another type assertion. If it fails, ok will be false, and e will be nil. If it succeeds, ok will be true, which means the error was of type *os.PathError
, and then so is e, which we can examine for more information about the error.
此处第二个 if 语句属于类型断言。如果失败,ok 将是 false,则 e 为 nil。如果成功,ok 将会是 true,这意味着 error 的动态类型是 *os.PathError
,并且会赋值给 e,也就是说,我们可以从中获取关于错误的更多细节了。
The usual way to report an error to a caller is to return an error
as an extra return value. The canonical Read
method is a well-known instance; it returns a byte count and an error
. But what if the error is unrecoverable? Sometimes the program simply cannot continue.
向调用者返回错误的寻常做法是返回一个额外的 error
值。典型的 Read
方法就是个广为人知的例子,它返回一个已读字节数和一个 error
。但是,如果错误是不可恢复的又将如何呢?有时候因为这些错误,程序根本无法继续运行。
For this purpose, there is a built-in function panic
that in effect creates a run-time error that will stop the program (but see the next section). The function takes a single argument of arbitrary type—often a string—to be printed as the program dies. It's also a way to indicate that something impossible has happened, such as exiting an infinite loop.
为解决此问题,go 提供了一个内建函数 panic
,事实上它会制造一个运行时错误,进而使程序停止运行(不要恐慌,下一章节有应对之法)。该函数接收一个任意类型的参数(通常都是 string),并在程序死亡之际将其打印出来。这种方式也表明发生了一些不可能的事,例如无限循环退出了。
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
This is only an example but real library functions should avoid panic
. If the problem can be masked or worked around, it's always better to let things continue to run rather than taking down the whole program. One possible counterexample is during initialization: if the library truly cannot set itself up, it might be reasonable to panic, so to speak.
此处仅举例说明,真正的库函数应避免 panic
。如果问题可以被掩盖或者解决,最好还是让程序继续运行而非粗暴的终结它。初始化过程或许是个例外:如果一个库确实无法设置自身,可以说,panic
就是合情合理的。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
When panic
is called, including implicitly for run-time errors such as indexing a slice out of bounds or failing a type assertion, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. If that unwinding reaches the top of the goroutine's stack, the program dies. However, it is possible to use the built-in function recover
to regain control of the goroutine and resume normal execution.
无论是类似 slice 越界访问的运行时错误还是类型断言失败,panic
一旦被调用,就会立即结束当前函数的执行,并展开 goroutine 的调用栈,一路执行所有被延期的函数。若展开的过程一路抵达 goroutine 调用栈的栈顶,则程序退出。不过,想重获 goroutine 的控制权使程序恢复正常执行也是可能的,使用内建函数 recover
。
A call to recover
stops the unwinding and returns the argument passed to panic
. Because the only code that runs while unwinding is inside deferred functions, recover
is only useful inside deferred functions.
调用 recover
会阻止调用栈的展开,且返回此前传给 panic
的参数,因为调用栈展开过程中唯一可以被执行的代码就是内部被 defer 的代码,所以只用在 defer
函数中。
One application of recover
is to shut down a failing goroutine inside a server without killing the other executing goroutines.
recover
的应用场景之一就是在 server
内部关闭失败的 goroutine 从而不影响其它正在运行的 goroutine。
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
In this example, if do(work)
panics, the result will be logged and the goroutine will exit cleanly without disturbing the others. There's no need to do anything else in the deferred closure; calling recover
handles the condition completely.
此例中,如果 do(work)
发生 panic,safelyDo
所在的 goroutine 会将错误结果打印到日志并且干净地退出,不会干扰其它的 goroutine。无需在延迟闭包函数中做任何事,只需调用 recover
就会完全处理好各种情况。
Because recover
always returns nil
unless called directly from a deferred function, deferred code can call library routines that themselves use panic
and recover
without failing. As an example, the deferred function in safelyDo
might call a logging function before calling recover
, and that logging code would run unaffected by the panicking state.
除非直接从延迟函数调用,否则 recover
总是返回 nil
,所以延迟代码可以调用本身使用 panic
和 recover
的库例程而不会出错。 例如 safelyDo 中的延迟函数或许会在调用 recover 之前调用 logging 函数,而且 logging 函数的代码并不受 panic 状态的影响。
With our recovery pattern in place, the do
function (and anything it calls) can get out of any bad situation cleanly by calling panic
. We can use that idea to simplify error handling in complex software. Let's look at an idealized version of a regexp
package, which reports parsing errors by calling panic
with a local error type. Here's the definition of Error
, an error
method, and the Compile
function.
恢复模式即已完备,do
函数(实际上任何调用)就可以通过调用 panic
来彻底摆脱各种糟糕的情况。我们在设计复杂软件的错误处理时可借鉴此思想。让我们一起看一下 regexp
包的理想版本,它通过 panic
报告解析错误。下面是类型 Error
,方法 error
,以及函数 Compile
的定义。
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
If doParse
panics, the recovery block will set the return value to nil
—deferred functions can modify named return values. It will then check, in the assignment to err
, that the problem was a parse error by asserting that it has the local type Error
. If it does not, the type assertion will fail, causing a run-time error that continues the stack unwinding as though nothing had interrupted it. This check means that if something unexpected happens, such as an index out of bounds, the code will fail even though we are using panic
and recover
to handle parse errors.
如果 doParse
发生 panic,恢复区的代码将返回值设置为 nil
——延迟函数可修改命名返回值。接下来会通过类型断言来检查捕获的错误是否为 Error
,如果不是,则断言失败,将产生一个运行时错误,然后调用栈的展开得以继续进行,就像从未被打断一样。尽管我们使用 panic
和 recover
处理了解析时的错误,但如果有些意料之外事情发生,比如说索引越界,此时,代码仍需要以失败的方式向上层 panic,这就是类型断言检查的意义所在。
With error handling in place, the error
method (because it's a method bound to a type, it's fine, even natural, for it to have the same name as the builtin error
type) makes it easy to report parse errors without worrying about unwinding the parse stack by hand:
有了错误处理方式的加持,error
方法可轻而易举地报告解析错误,不必再担心手动展开解析堆栈:
译注:此处的 unwinding the parse stack by hand 应指在解析过程中的错误处理,解析过程肯定涉及到多层方法或者函数调用,这些叫做
解析堆栈调用
,如果不使用panic
的话,你需要在每层调用里手动处理错误,即令人厌恶的if err != nil
。
if pos == 0 {
re.error("'*' illegal at start of expression")
}
Useful though this pattern is, it should be used only within a package. Parse
turns its internal panic
calls into error
values; it does not expose panics
to its client. That is a good rule to follow.
此方法虽好,但只应在包内部使用。Parse
把内部的 panic
调用转换为 error
,并没有 panic 给它的使用者(包外)。应遵循这个良性规则。
By the way, this re-panic idiom changes the panic value if an actual error occurs. However, both the original and new failures will be presented in the crash report, so the root cause of the problem will still be visible. Thus this simple re-panic approach is usually sufficient—it's a crash after all—but if you want to display only the original value, you can write a little more code to filter unexpected problems and re-panic with the original error. That's left as an exercise for the reader.
顺便一提,当有除解析错误之外的真正 panic 发生时,这种重复 panic 的风格实际上改变了 panic 的值。不过,两种失败原因都将在故障报告中呈现出来,因此问题的根因将仍是可见的。因此,这种简单的 re-panic 的方式通常情况下是足够的——毕竟这是个crash —— 但是,如果你只是想显示原始值,你可以多写一点代码来过滤不想要的问题,之后再将原始错误 panic 出去。这就当做练习留给各位读者吧。