前言
本文主要讲解Golang的异常处理,涉及error类型、defer、panic、以及recover关键字。
注:其实准确来说,error属于接口(interface),panic和recover属于函数(function),而defer属于关键字
error的使用
关于error的使用推荐阅读 Golang error 的突围这篇文章,总的来说,error的使用有以下几点:
1.需要区分错误和异常两个概念,即什么时候使用error、什么时候使用panic,比较简单的办法是"不会终止程序逻辑运行的归类为错误,会终止程序逻辑运行的归类为异常"。
2.Go的错误处理 (error) 总是显得又丑又长,但如果跟 try...catch 机制比起来,个人还是喜欢第一种,因为可以把所有错误都集中在最上层(业务分层),在最上层进行错误日志记录;使用try...catch容易导致日志东一处、西一处。
3.一般对error的判断都是直接判定是否为nil,而不是判断具体的错误,比如err == io.EOF,好处是可以减少耦合性。
4.error通过返回字符串说明错误的原因,但有时仅有字符串说明还是不太够,Go也提供了其他方式,具体可以看上述的文章。
defer的使用
defer关键字后面接的是一个执行函数,执行的函数会在return语句执行之后,如果有多个defer声明的函数,则会按照FILO(先进后出)的顺序执行。
defer的原理其实很简单,就是压栈,编译器遇到defer,就把defer对应的函数压入栈中,这也是defer先进后出的原因。
defer 的常见用法就是关闭某个操作,比如关闭文件描述符,执行完数据库查询后关闭数据库连接等,下面是几个例子。
1.关闭文件描述符
// CopyFile 这个函数是有bug的,是作为一个反例。
// 因为当os.Create(dstName) 创建失败时,函数就直接返回了, 此时却没有关闭src和dst文件描述符。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
// 我们也可以在这里手动关闭,但是我们容易忽略这种情况
// dst.Close()
// src.Close()
return
}
written, err = io.Copy(dst, src)
// 在这里关闭,如果Create失败,无法关闭src描述符
dst.Close()
src.Close()
return
}
// 加了defer的函数, 不会有上述的问题
func CopyFileDefer(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
written, err = io.Copy(dst, src)
return
}
2.关闭数据库连接
对于 database/sql 这个库,有一个常犯的错误(我就犯过),就是查询数据时,由于忘记关闭数据库连接导致内存泄漏(如果没有关闭数据库连接,每一次新的查询内存中就多了一条数据库连接,连接一多内存就爆了)
rows, err := db.Query("select id, name from users")
if err != nil {
log.Fatal(err)
}
// 这条语句就是用来关闭数据库连接
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
return err
}
log.Println(id, name)
}
具体的原因是结果集(rows)未关闭前,底层的连接处于繁忙状态。当使用Next()函数遍历数据并读到最后一条记录时,会发生一个内部EOF错误,自动调用rows.Close();但是如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭,导致内存泄漏。
所以手动关闭非常重要。rows.Close()可以多次调用,是无害操作。
3.使用defer时的几个注意点:
3.1 defer关键字声明的函数会延迟执行,但是函数内的参数会立即求值
func deferFIFO(n int) int {
defer func() {
n++
fmt.Println("3st:", n)
}()
defer func() {
n++
fmt.Println("2st:", n)
}()
defer func() {
n++
fmt.Println("1st:", n)
}()
return n
}
func deferFIFOTest() {
//1st: 2
//2st: 3
//3st: 4
deferFIFO(1)
}
3.2 由于defer定义的函数会延迟执行,如果在该函数内操作返回值,则返回值会改变
func modifyReturnV() (i int) {
defer func() { i++; fmt.Println(i) }()
return 2
}
func modifyReturnVTest() {
// 结果为 3
fmt.Println("value", modifyReturnV())
}
panic和recover的使用
关于error的使用推荐阅读 golang 中的 panic 和 recover这篇文章,总的来说,panic的使用有以下几点:
1.当遇到不可恢复的错误,比如端口绑定失败等,这种情况程序继续执行也没啥意义,所以建议使用panic
2.recover用于从panic的中断状况中恢复,但仅当recover函数定义在defer函数内的时候
func f() {
defer func() {
if r := recover(); r != nil{
fmt.Println("recovered in f", r)
}
}()
fmt.Println("Calling g")
g(0)
fmt.Println("Returned normally from g")
}
func g(i int) {
if i > 3{
fmt.Println("Panicking!")
// 本例中, fmt.Sprintf会返回一个值, 这个值会传递给panic, 进而被recover捕获。
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i+1)
}
func main() {
f()
fmt.Println("Returned normally from f")
}
3.recover仅在从同一个goroutine调用时才起作用。从不同的goroutine触发的panic中recover是不可能的(具体参见上述文章)。