Go语言学习之路第8天(异常处理)
Posted dacaigouzi1993
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言学习之路第8天(异常处理)相关的知识,希望对你有一定的参考价值。
一.异常处理
所谓的异常:当GO检测到一个错误时,程序就无法继续执行了,反而出现了一些错误的提示,这就是所谓的"异常"。
所以为了保证程序的健壮性,要对异常的信息进行处理。例如,如下程序,定义一个函数实现整除操作,这个程序对大家来说已经很简单了,实现如下:
func Test(a, b int) int var result int result = a / b return result
但是,大家仔细考虑一下,该方法是否有问题?
如果b的值为0,会出现什么情况?
程序会出现以下的异常信息:
panic: runtime error: integer divide by zero
并且整个程序停止运行。
那么出现这种情况,应该怎样进行处理呢?这时就要用到异常处理方法的内容。
1.1 error接口
Go语言引入了一个关于错误处理的标准模式,即error接口,它是Go语言内建的接口类型,该接口的定义如下:
type error interface Error() string
Go语言的标准库代码包errors为用户提供如下方法:
package errors // New returns an error that formats as the given text. func New(text string) error return &errorStringtext // errorString is a trivial implementation of error. type errorString struct s string func (e *errorString) Error() string return e.s
通过以上代码,可以发现error接口的使用是非常简单的(error是一个接口,该接口只声明了一个方法Error(),返回值是string类型,用以描述错误)。下面看一下基本使用,
首先导包:
import "errors"
然后调用其对应的方法:
func main() err := errors.New("This is a normal err") fmt.Println("err:", err.Error())
当然fmt包中也封装了一个专门输出错误信息的方法,如下所示:
err := fmt.Errorf("%s", "This is a normal err") fmt.Println("err:", err)
了解完基本的语法以后,接下来使用error接口解决Test( )函数被0整除的问题,如下所示:
func Test(a, b int) (result int, err error) err = nil if b == 0 err = errors.New("除数不能为0") else result = a / b return result, err func main() res, err := Test(10, 0) if err != nil fmt.Println("err:", err) else fmt.Println(res)
在Test( )函数中,判断变量b的取值,如果有误,返回错误信息。并且在main( )中接收返回的错误信息,并打印出来。
这种用法是非常常见的,例如,后面讲解到文件操作时,涉及到文件的打开,如下:
func Open(name string) (*File, error)
在打开文件时,如果文件不存在,或者文件在磁盘上存储的路径写错了,都会出现异常,这时可以使用error记录相应的错误信息。
1.2 panic函数
error返回的是一般性的错误,但是panic函数返回的是让程序崩溃的错误。也就是当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起painc异常,在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic。
一般而言,当panic异常发生时,程序会中断运行。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。
当然,如果直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。
下面给大家演示一下,直接调用panic函数,是否会导致程序的崩溃。
func TestA() fmt.Println("func TestA()") func TestB() panic("func TestB(): panic") func TestC() fmt.Println("func TestcC()") func main() TestA() TestB() //TestB()发生异常,中断程序 TestC()
错误信息如下:
panic: func TestB(): panic func TestA() goroutine 1 [running]: main.TestB(...) /Users/guanyuji/go/src/awesomeProject/Go基础班第8天/01.go:45 main.main() /Users/guanyuji/go/src/awesomeProject/Go基础班第8天/01.go:54 +0x98 Process finished with exit code 2
所以,我们在实际的开发过程中并不会直接调用panic( )函数,但是当我们编程的程序遇到致命错误时,系统会自动调用该函数来终止整个程序的运行,也就是系统内置了panic函数。
下面给大家演示一个数组下标越界的问题:
func TestA() fmt.Println("func TestA()") func TestB(x int) var a [10]int a[x] = 222 func TestC() fmt.Println("func TestcC()") func main() TestA() TestB(11) TestC()
错误信息如下:
func TestA() panic: runtime error: index out of range goroutine 1 [running]: main.TestB(...) /Users/guanyuji/go/src/awesomeProject/Go基础班第8天/01.go:46 main.main() /Users/guanyuji/go/src/awesomeProject/Go基础班第8天/01.go:55 +0x7d Process finished with exit code 2
通过观察错误信息,发现确实是panic异常,导致了整个程序崩溃。
1.3 延迟调用defer
(1)defer的基本使用
函数定义完成后,只有调用函数才能够执行,并且一经调用立即执行。例如:
fmt.Println("hello") fmt.Println("world")
先输出"hello",再输出"world"。
但是关键字defer ?于延迟一个函数(或者当前所创建的匿名函数)的执行(注意,defer语句只能出现在函数的内部)。将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中,defer语句可以充当其他语言中try…catch…的角色,也可以用来处理关闭文件句柄等收尾操作。
基本用法如下:
defer fmt.Println("hello") fmt.Println("world")
以上两行代码,输出的结果为,先输出"world",再输出"hello"。
(2)defer触发时机
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
Go官方文档中对defer的执行时机做了阐述,分别是。
- 包裹defer的函数返回时
- 包裹defer的函数执行到末尾时
- 所在的goroutine发生panic时
(3)defer的执行顺序
一个方法中有多个defer时, defer会将要延迟执行的方法“压栈”,当defer被触发时,将所有“压栈”的方法“出栈”并执行。所以defer的执行顺序是LIFO(先进后出)的。
所以下面这段代码的输出不是1 2 3,而是3 2 1。
func main() defer func() fmt.Println(1) () defer func() fmt.Println(2) () defer func() fmt.Println(3) ()
(4)defer与return,函数返回值之间的顺序
先说结论:return最先执行->return负责将结果写入返回值中->接着defer开始执行一些收尾工作->最后函数携带当前返回值退出
返回值的表达方式,我们知道根据是否提前声明有两种方式:一种是func test() int 另一种是 func test() (i int),所以两种情况都来说说:
先看一下:func test() int
func Test() int i := 0 defer func() i++ fmt.Println("defer2的值:", i) () defer func() i++ fmt.Println("defer1的值:", i) () return i func main() fmt.Println("main:", Test())
结果如下:
defer1的值: 1 defer2的值: 2 main: 0
上面函数Test的返回值属于匿名返回值,返回值在return的时候才确定下来是i的值,具体过程如下:
- 将i赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = i)
- 然后检查是否有defer,如果有则执行
- 返回刚才创建的返回值(retValue)
在这种情况下,defer中的修改是对i执行的,而不是retValue,所以defer返回的依然是retValue。
再看一下:func test() (i int)
func Test() (i int) defer func() i++ fmt.Println("defer2的值:", i) () defer func() i++ fmt.Println("defer1的值:", i) () return i func main() fmt.Println("main:", Test())
结果如下:
defer1的值: 1 defer2的值: 2 main: 2
这里的函数Test的返回值属于命名返回值,在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,i就是retValue,defer对于i的修改也会被直接返回。
(5)defer的定义和执行是两个步骤
先说结论:会先将defer后函数的参数部分的值(或者地址)给先记下来【你可以理解为()里头参数的值的会先确定】,后面函数执行完,才会执行defer后函数的中的逻辑。
func test(i int) int return i func main() var i int = 1 //defer定义时候test(i)的值就已经确定了,是1,之后就不会变了 defer fmt.Println("i1 = ",test(i)) i++ //defer定义时候test(i)的值就已经确定了,是2,之后就不会变了 defer fmt.Println("i2 = ",test(i)) //defer在定义的时候,i就已经确定了是一个指针类型,地址上的值变了,这里跟着变,是2 defer func(i *int) fmt.Println("i3 = ",*i) (&i) //defer在定义的时候,i就已经确定了,是2,之后就不会变了 defer func(i int) fmt.Println("i4 = ",i) (i) defer func() // 地址,所以后续跟着变 var c = &i fmt.Println("i5 =" , *c) () // 执行了 i=11 后才调用,此时i值已是11 defer func() fmt.Println("i6 =" , i) () i = 11
结果如下:
i6 = 11 i5 = 11 i4 = 2 i3 = 11 i2 = 2 i1 = 1
(6)尽量避免在for循环中使用defer
看下面的代码:
func DeferTest() for i := 0;i < 100;i++ f,_ := os.Open("/etc/hosts") defer f.Close() func main() DeferTest()
这是一个循环可打开文件的函数(文件操作之后讲到),defer在紧邻创建资源的语句后声明,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。
(7)判断执行没有err之后,再defer释放资源
一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。
正确写法如下。
func main() fr,err := os.Open("/etc/hosts") if err != nil fmt.Println("os.Open err",err) return defer fr.Close()
(8)调用os.Exit时defer不会被执行
os.Exit()的作用是种植当前整个程序,当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。unc DeferTest() defer func() fmt.Println("defer") () os.Exit(0) func main() DeferTest()
上面的defer并不会输出。
1.4 recover
运行时panic异常一旦被引发就会导致程序崩溃。这当然不是我们愿意看到的,但谁也不能保证程序不会发生任何运行时错误。
Go语言为我们提供了专用于"拦截"运行时panic的内建函数——recover。它可以让当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
语法如下:
func recover() interface
注意:recover只有在defer调用的函数中才有效
示例如下:
func TestA() fmt.Println("TestA") func TestB(i int) //设置recover defer func() recover() //防止程序崩溃 () //()一定要加上,调用此匿名函数 var a [10]int a[i] = 222 //当i=11时,会造成数组下标越界,引起panic func TestC() fmt.Println("TestC") func main() TestA() TestB(11) TestC()
结果如下:
TestA TestC
通过以上程序,我们发现虽然TestB( )函数会导致整个应用程序崩溃,但是由于在该函数中调用了recover( )函数,所以整个函数并没有崩溃。虽然程序没有崩溃,但是我们也没有看到任何的提示信息,那么怎样才能够看到相应的提示信息呢?
可以直接打印recover( )函数的返回结果,如下所示:
func TestA() fmt.Println("TestA") func TestB(i int) //设置recover defer func() fmt.Println(recover()) //防止程序崩溃 () //()一定要加上,调用此匿名函数 var a [10]int a[i] = 222 //当i=11时,会造成数组下标越界,引起panic func TestC() fmt.Println("TestC") func main() TestA() TestB(11) TestC()
结果如下:
TestA runtime error: index out of range TestC
从输出结果发现,确实打印出了相应的错误信息。
但是,如果程序没有出错,也就是数组下标没有越界,会出现什么情况呢?
func TestA() fmt.Println("TestA") func TestB(i int) //设置recover defer func() fmt.Println(recover()) //防止程序崩溃 () //()一定要加上,调用此匿名函数 var a [10]int a[i] = 222 //当i=11时,会造成数组下标越界,引起panic func TestC() fmt.Println("TestC") func main() TestA() TestB(1) //没有越界 TestC()
结果如下:
TestA <nil> TestC
这时输出的是空,但是我们希望程序没有错误的时候,不输出任何内容。
所以,程序修改如下:
func TestA() fmt.Println("TestA") func TestB(i int) //设置recover defer func() //判断是否出现错误 if err := recover();err != nil fmt.Println(err) //防止程序崩溃 () //()一定要加上,调用此匿名函数 var a [10]int a[i] = 222 //当i=11时,会造成数组下标越界,引起panic func TestC() fmt.Println("TestC") func main() TestA() TestB(11) //没有越界 TestC()
通过以上代码,发现其实就是加了一层判断。
最后还要注意:recover一定要放在可能会发生异常的代码段前面。
以上是关于Go语言学习之路第8天(异常处理)的主要内容,如果未能解决你的问题,请参考以下文章