zoukankan      html  css  js  c++  java
  • 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 &errorString{text}
    }
    
    // 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一定要放在可能会发生异常的代码段前面。

  • 相关阅读:
    codeforces 719A:Vitya in the Countryside
    POJ3233 Matrix Power Series
    codevs1409 拦截导弹2
    BZOJ1562 [NOI2009]变换序列
    POJ1325 Machine Schedule
    codeforces 715B:Complete The Graph
    BZOJ1972:[SDOI2010]猪国杀
    浅谈模拟
    BZOJ2548:[CTSC2002]灭鼠行动
    BZOJ1033:[ZJOI2008]杀蚂蚁
  • 原文地址:https://www.cnblogs.com/dacaigouzi1993/p/11129983.html
Copyright © 2011-2022 走看看