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一定要放在可能会发生异常的代码段前面。

原文地址:https://www.cnblogs.com/dacaigouzi1993/p/11129983.html

时间: 2024-10-09 08:56:38

Go语言学习之路第8天(异常处理)的相关文章

我的C语言学习之路——初装Linux(双系统模式)

我的C语言学习之路--初装Linux(双系统模式) 第一次听说Linux是在微软公布不再维护WindowXP系统,当时打开头条网,Linux几乎占据了头条,好奇之下,百度了Linux(Linux操作系统是UNIX操作系统的一种克隆系统,它诞生于1991 年的 [Linux桌面] 10 月5 日(这是第一次正式向外公布的时间)). 第二次听说Linux是在程序员联盟(微信公众号:programmerLeague)   讨论,这也是我真正接触Linux的时候.作为一个编程初学者,对一切有利于编译的软

C语言学习之路

C语言学习之路之环境准备 Hello,大家好,我的专业是软件工程,自从大一的时候,和很多人一样,在懵懂中学习了C语言~哈,不过呢,这门语言是真的很基础,也很重要!所以呢,如果是想要在IT行业大显身手的话,就必须对这门语言有所了解.那么,开始我们的探索之路吧,GO~ 工欲善其事,必先利其器!要学好这门语言,我们需要几个工具:一,是一本参考书,本人推荐使用<C Primer Plus>这本书,个人觉得这本书写的很好,推荐大家入门使用,还有一本书是<深入理解C指针>:二,是一款好的编译器

c语言学习之路提升

这几天 把C语言的基础学习了 在网上查了查 想要提升自己 还要学习 数据结构和算法 从明天 开始 我将会 继续学习 c 语言的数据结构 和算法

C语言学习之路(1)

学习C语言后我以为我就可以永久的告别单引号了,哎学习还不到一周就碰到了C语言中用单引号的情况,着实一惊. for(i=0;* (p+i)!='\0';i++) * (q+i)=* (p+i); * (q+i)='\0' 刚开始我用的是双引号 for(i=0;* (p+i)!="\0";i++) * (q+i)=* (p+i); * (q+i)="\0" 程序一运行,呵,蹦出个段错误,天哪,我学习C语言还不到一周就让我碰到了段错误,我这是造的什么孽啊,后来左思右想(看

C语言学习之路——指针初探

进来学习C语言碰到了指针,着实学了一段时间,搞不出个头绪,今日感觉有些眉目,来此做个记录,也望能给困顿的人一起分享下感悟. 学习指针还要从变量,数组,函数的定义这些基础说起. 如:int a=10; int a[]={1,2,3,4}; int max (int x,int y) { .... } 学习C语言指针我想指针变量的定义就不用说了吧. 先上一个例子然后再给大家一一展开进行讲述. #include<stdio.h>int main(){int a;scanf("%d"

go语言学习之路 一:开发环境配置

1. 安装go 1)下载地址:http://www.golangtc.com/download,下载后直接双击msi文件安装,默认安装在c:\go 2)安装完成后默认会在环境变量 Path 后添加 Go 安装目录下的 bin 目录 C:\Go\bin\,并添加环境变量 GOROOT,值为 Go 安装根目录 C:\Go\. 3)设置工作空间gopath目录(Go语言开发的项目路径) Windows 设置,新建一个环境变量名称叫做GOPATH,值为你的工作目录,例如笔者的设置GOPATH=e:\go

go语言学习之路(二)

Go 语言条件语句 条件语句需要开发者通过指定一个或多个条件,并通过测试条件是否为 true 来决定是否执行指定语句,并在条件为 false 的情况在执行另外的语句. 下图展示了程序语言中条件语句的结构: Go 语言提供了以下几种条件判断语句: if语句             -->   if 语句 由一个布尔表达式后紧跟一个或多个语句组成 if嵌套语句  -->   你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句. if .. else      

C语言学习之路,第一篇章。

看的书是 C  primer plus  ,这本书好评很多, 学过C#,没有精通,了解Java,所以看这本书会很容易上手,编译器用的是VC++6.0,因为VS2010好像不支持C99标准,有些代码功能会报错,所以新手的话还是用专业写C的IDE吧, 加油,准备先看C,在看,汇编和数据结构,然后进军C++,之后看ISO编程,. 先打好基础重要,.在此写下记录,激励自己努力学习.

Go语言学习之路-2-变量与常量

标识符与关键字 标识符 定义变量时,我们定义了: name.age 这样的名字,它们都是程序员自己起的,能够表达出变量的作用,这叫做标识符(Identifier) 标识符就是程序员自己起的名字,名字也不能随便起,要遵守规范,在Go语言中标识符由 只能以“字母”和“_”开头 由“字母”.“数字”.“_”组成 Go语言保留的标识符,25个相对其他语言来很少 func return defer go if else for range continue goto break switch select