Go语言学习 二十三 错误处理和运行时恐慌(Panic)

本文最初发表在我的个人博客,查看原文,获得更好的阅读体验


一 错误

1.1 error类型

按照约定,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() }

该错误详细的描述了引发错误的操作以及相关文件路径和错误描述信息。

1.2 其他错误类型

除此之外,标准库中还有许多其他预定义的错误类型,它们都直接或间接的实现(或内嵌)了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(恐慌)

内建函数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(恢复)

内建函数recover可以让发生panickinggoroutine恢复正常运行。在一个被推迟的函数中执行recover可以终止panic的产生的终止回溯调用。注意必须是直接在被推迟的函数中。如果不是在推迟函数中(或间接)调用该函数,则不会发生任何作用,将返回nil。如果程序没有发生panicpanic的参数为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来检查它。若它没有,类型断言将会失败,此时会产生运行时错误,并继续栈的回溯,仿佛一切从未中断过一样。该检查意味着若发生了一些像索引越界之类的意外,那么即便我们使用了panicrecover来处理解析错误,代码仍然会失败。

通过适当的错误处理,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

你可能感兴趣的:(go)