本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
按照约定,Go中的错误类型为error
,这是一个内建接口,nil
值表示没有错误:
type error interface {
Error() string
}
我们可以很方便的自定义一个错误类型:
package main
import (
"fmt"
)
func main() {
e := MyError{"This is a custom Error Type."}
fmt.Println(e.Error())
v1, err := divide(10, 2)
if err == nil {
fmt.Println(v1)
}
if v2, err := divide(5, 0); err != nil {
fmt.Println(err)
} else {
fmt.Println(v2)
}
}
// 自定义错误类型
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
// 取整除法
func divide(a1, a2 int) (int, error) {
if a2 == 0 {
return 0, &MyError{"整数相除,除数不能为零"}
}
return a1 / a2, nil
}
上述divide
函数会返回一个error
值,调用方可以根据这个错误值来判断如何处理结果。这种用法在Go中是一种惯用法,尤其在编写一些函数库之类的功能时。例如标准库os
中的打开文件的Open
函数定义如下:
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
该函数返回的具体错误类型为PathError
:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
该错误详细的描述了引发错误的操作以及相关文件路径和错误描述信息。
除此之外,标准库中还有许多其他预定义的错误类型,它们都直接或间接的实现(或内嵌)了error
接口。例如:
runtime.Error // 表示运行时错误的Error接口类型
net.Error // 表示网络错误的Error接口类型
go/types.Error // 表示类型检查错误的Error结构类型
html/template.Error // 表示html模板转义期间遇到的问题(结构类型)
os/exec // 当文件不是一个可执行文件时返回的错误(结构类型)
另外,标准库中的errors
包提供了一个函数可方便地返回一个error
实例:
// New 函数返回格式为给定文本的错误
func New(text string) error
它的具体实现如下:
// errors 包实现了操作错误的函数
package errors
// New 函数返回格式为给定文本的错误
func New(text string) error {
return &errorString{text}
}
// errorString 是 error 的一个简单实现(注意是私有的)
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
示例:
package main
import (
"errors"
"fmt"
)
func main() {
fmt.Println(errors.New("这是一条错误信息"))
}
如果上述错误描述过于简单,还可以使用fmt
包中的Errorf
函数:
// Errorf根据指定的格式进行格式化参数,并返回满足error接口的字符串
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}
该函数允许我们使用软件包的格式化功能来创建描述性错误消息:
package main
import (
"fmt"
)
func main() {
const name, id = "bimmler", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
if err != nil {
fmt.Print(err)
}
}
通常,以上两种方法能满足绝大多数错误场景。如果仍然不够,正如本文开头所讲,你可以自定义任意的错误类型。
内建函数panic
可以产生一个运行时错误,一旦调用该函数,当前goroutine
就会停止正常的执行流程。这种情况一般发生在一些重要参数缺失的检查时,因为如果缺失了这些参数,将导致程序不能正常运行,故相比让程序继续运行(也可能根本就没法正常运行),不如让它及时终止。
func panic(v interface{})
该函数接受一个任意类型的实参(一般为字符串),并在程序终止时打印。
package main
func main() {
panic("运行出错了。")
}
另一类使用场景:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("wait for init...")
}
var user = os.Getenv("USER")
func init() {
// 检查必要变量等
if user == "" {
panic("no value for $USER")
}
}
一般情况下,我们应避免使用panic,尤其是在库函数中。
当panic
被调用后(包括不明确的运行时错误,例如数组或切片索引越、类型断言失败)等,程序将立刻终止当前函数的执行,并开始回溯goroutine
的栈,运行任何被推迟的函数。若回溯到达goroutine
栈的顶端,程序就会终止。
假设函数F
调用了panic
,则F
的正常执行将立即停止。F
中任何被推迟的函数将依次执行,然后F
返回到调用处。对于调用者G
,此时好像也在调用panic
函数一样,执行到此立即停止,并开始回溯所有被G
推迟的函数。就这样一直回溯,直到该goroutine
中的所有函数都停止,此时,程序终止,并报告错误信息,包括传给panic
的参数。
当然,我们还可以使用内建函数recover
进行恢复,夺回goroutine
的控制权,继续往下看。
我们在defer语句一文中提到过,
defer
栈是以LIFO
的顺序执行的。
内建函数recover
可以让发生panicking
的goroutine
恢复正常运行。在一个被推迟的函数中执行recover
可以终止panic
的产生的终止回溯调用。注意必须是直接在被推迟的函数中。如果不是在推迟函数中(或间接)调用该函数,则不会发生任何作用,将返回nil
。如果程序没有发生panic
或panic
的参数为nil
,则recover
的返回值也为nil
。
func recover() interface{}
以下示例展示了panic和recover的工作机制:
package main
import "fmt"
func main() {
f()
fmt.Println("从 f() 中正常返回。")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("从 f() 中正常恢复。", r)
}
}()
fmt.Println("开始调用函数 g()。。。")
g(0)
fmt.Println("从 g() 中正常返回。")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("函数g()中推迟的调用", i)
fmt.Println("函数g()中的打印", i)
g(i + 1)
}
func h() {
fmt.Println("hello")
}
看一个effective_go中的例子:
在服务器中终止失败的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)
}
通过恰当地使用恢复模式,do
函数(及其调用的任何代码)可通过调用panic
来避免更坏的结果。我们可以利用这种思想来简化复杂软件中的错误处理。
再看一个regexp
包的理想化版本,它会以局部的错误类型调用panic
来报告解析错误。以下是一个Error
类型,一个error
方法和一个Compile
函数的定义:
// Error 是解析错误的类型,它满足 error 接口。
type Error string
func (e Error) Error() string {
return string(e)
}
// error 是 *Regexp 的方法,它通过用一个 Error 触发Panic来报告解析错误。
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile 返回该正则表达式解析后的表示。
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// 如果有解析错误,doParse会产生panic
defer func() {
if e := recover(); e != nil {
regexp = nil // 清理返回值
err = e.(Error) // 若它不是解析错误,将重新触发Panic。
}
}()
return regexp.doParse(str), nil
}
如果doParse
触发了panic
,恢复块会将返回值设为nil
—被推迟的函数能够修改已命名的返回值。在err
的赋值过程中,我们将通过断言它是否拥有局部类型Error
来检查它。若它没有,类型断言将会失败,此时会产生运行时错误,并继续栈的回溯,仿佛一切从未中断过一样。该检查意味着若发生了一些像索引越界之类的意外,那么即便我们使用了panic
和recover
来处理解析错误,代码仍然会失败。
通过适当的错误处理,error
方法(由于它是个绑定到具体类型的方法,因此即便它与内建的error
类型名字相同也没有关系)能让报告解析错误变得更容易,而无需担心手动处理回溯的解析栈:
if pos == 0 {
re.error("'*' illegal at start of expression")
}
尽管这种模式很有用,但它应当仅在包内使用。Parse
会将其内部的panic
调用转为error
值,它并不会向调用者暴露出panic
。这是个值得遵守的良好规则。
另外,这种重新触发panic
的惯用法会在产生实际错误时改变panic
的值。然而,不管是原始的还是新的错误都会在崩溃报告中显示,因此问题的根源仍然是可见的。这种简单的重新触发panic
的模型已经够用了,毕竟它只是一次崩溃。但若你只想显示原始的值,也可以多写一点代码来过滤掉不需要的问题,然后用原始值再次触发panic
。
参考:
https://golang.org/doc/effective_go.html#errors
https://golang.org/pkg/builtin/#error
https://blog.golang.org/defer-panic-and-recover