Go语言-defer与异常处理

defer与异常处理

在本篇文章中,介绍关于Go语言中defer的使用以及注意事项,还有如何在Go语言中使用defer去处理异常。defer是Go语言提供的关键字,它的作用是,当你声明了注册了defer语句,那么该语句会在函数返回时被执行。

为什么要使用defer

C\C++或者其它需要手动管理某些资源(包括但不限于内存、文件描述符等)的语言中,我们可能会存在以下情况,那就是我们开辟了一块新的内存,或者打开了一个新的文件描述符,刚开始我们可能记着要释放,但是写代码写着写着,写到最后我们可能就忘了有这么一回事了,这样就可能导致资源泄露,所以各种语言都提供了一种或者多种机制,用于在代码块结束、函数调用结束、变量生命周期结束的时机释放资源。例如C++提供了RAII用于当前对象声明周期结束后使用析构函数释放资源、Rust提供了生命周期的方式用来保证资源正确释放,而defer就是Go语言提供的用于释放资源的方式。

defer在Go语言中,可以用于释放互斥锁、增加waitGroup的计数、回滚数据库事务以及处理各种函数调用的Epilogue。虽然是手动的,但是功能非常强大。

怎样使用defer

defer的基础使用方式如下:

go 复制代码
func deferExample1() {
	defer fmt.Println("deferExample1's defer Invoke!")
	fmt.Println("deferExample1 Invoke!")
}

可以看到实现执行了函数内部的其他代码,最后才执行的defer语句,defer关键字之后不仅可以跟一条语句,还可以跟一个函数的调用语句,示例如下:

go 复制代码
func deferExample2() {
	defer func() {
		fmt.Println("deferExample2's defer function Invoke!")
	}()
	fmt.Println("deferExample2 Invoke!")
}

并且在一个函数中可以使用defer关键字注册多个函数的调用语句,或者普通语句,示例如下:

go 复制代码
func deferExample3() {
	defer fmt.Println("deferExample3's defer statement 1 execute!")
	defer func() {
		fmt.Println("deferExample3's defer function 2 execute!")
	}()
	defer func() {
		fmt.Println("deferExample3's defer function 3 execute!")
	}()
	defer fmt.Println("deferExample3's defer statement 4 execute!")
	fmt.Println("deferExample3 Invoke!")
}

这里我们可以看到,defer注册语句的执行顺序与注册顺序相反,我们是按照1,2,3,4的顺序注册的,而调用是按照4,3,2,1的顺序调用的。这是因为在Go语言的运行时在函数执行期间维护了一个"链栈",每次调用defer就会在这个"链栈"中新增一项,而我们知道栈这种数据结构具有FILO(First In Last Out)的特性的,所以最先注册的defer语句会被最后调用,具体的过程如下所示:

这张图清晰的解释了defer的整个工作流程,可以看到defer语句是如何注册,如何执行的。并且还有关键的一点就是,derfer并不是在函数返回完成之后执行的,而是在函数返回期间执行的 ,因为函数的返回并不是一个原子操作,返回期间要做很多事情,比如处理函数栈帧时,就要释放局部变量、给返回值寄存器赋值等等,所以defer语句也是在返回期间完成的。

使用defer的注意事项

在面试的时候关于defer关键字,除了上述的执行流程,一般还会有给你一段代码让你说出代码的执行结果这种问题,所以我们也来看看关于defer使用的细枝末节。

预计算参数

在使用defer时,这是一个比较常见的场景,就是在defer中使用一个当前函数的变量,以一个示例来了解:

go 复制代码
func deferExample4() {
	i := 5
	defer fmt.Println("deferExample4's defer i = ", i)
	i = 10
	fmt.Println("deferExample4's i = ", i)
}

我们可以看到这里函数内部打印的值为10,defer语句中打印的值为5,因为defer语句具有预计算参数的作用,意思就是我们在注册defer语句的时候,defer语句中的值就已经被计算出来了,所以defer语句中的i在注册时就被赋值了。

这种情况,与我们使用defer进行函数调用时,将函数体内部的变量以参数的方式传递到defer注册的函数中时一致,示例如下:

go 复制代码
func deferExample5() {
	i := 5
	defer func(i int) {
		fmt.Println("deferExample5's defer i = ", i)
	}(i)
	i = 10
	fmt.Println("deferExample5's i = ", i)
}

但是在使用闭包捕获函数体内的变量到defer注册的函数中时,情况就会发生变化,示例如下:

go 复制代码
func deferExample6() {
	i := 5
	defer func() {
		fmt.Println("deferExample6's defer i = ", i)
	}()
	i = 10
	fmt.Println("deferExample6's i = ", i)
}

在这里我们得到了相同的输出,这是因为defer函数在注册时将该变量捕获到函数内部了,而defer语句是在函数返回期间执行的,这里defer函数内部的i和函数内部的i指向的是同一个变量地址,所以defer函数会和函数打印同样的值。

不过在这里还有一种特殊情况,那就是当返回值是一个命名返回值时,它又会出现不同的结果,示例如下:

go 复制代码
func deferExample7() (i int) {
	i = 5
	defer func() {
		fmt.Printf("deferExample7's defer i = %d,addr = %p\n", i, &i)
	}()
	i = 10
	fmt.Printf("\"deferExample7's  i = %d,addr = %p\n", i, &i)
	i = 3
	return i + 2
}

如果根据上面的闭包规则,那么我们预期得到的结果应该是3,因为在最后3被赋予了i,但是由于这是一个命名返回参数,而defer语句又是在返回期间执行的,所以我们得到了5,用下面这张图来解释这个现象:

这样我们就清晰的知道,为什么用闭包捕获一个命名返回值会出现这种不同的作用了,这是由于defer语句是在函数返回期间执行的,而defer语句又在注册的时候闭包捕获了命名返回值i获得了i的地址,所以函数在返回的时候将返回值赋给命名返回值,此时命名返回值i的值就发生了变化,而赋予返回值之后,才开始执行函数调用的清理工作,比如释放栈帧和执行defer语句,所以defer语句中使用的命名返回值是最新的值5。

还有一点就是,我们在使用defer的时候,不能只在defer关键字之后定义一个匿名函数,而不调用它,这样是错误的。

综上所述,我们使用defer时要注意如下几点:

  • defer关键字之后只能跟一条语句,而不能跟一个匿名函数的定义!

  • defer语句是在函数返回过程中执行的,而不是函数返回后执行的。

  • defer语句具有预计算值的功能,当我们没有使用闭包捕获函数内部的变量时,defer语句在注册时就会计算出要使用的值。

  • defer函数闭包捕获函数内部的变量,在defer函数执行时会获取到该变量最新的值,因为defer函数获取了该变量的地址。

  • defer函数闭包捕获命名返回值时,如果函数内部显示指定 了返回值,那么这个返回值会作为命名返回值最新值defer函数在执行时也会使用最新的值。

什么是异常和错误

Go语言中的异常与错误和其他语言中的异常与错误有一定的区别,所以我们主要来了解在Go语言中什么是异常和错误,简单的来说,Go语言中的异常就是程序在运行过程中由panic引发异常,这种异常如果没有恢复则会导致程序崩溃,而Go语言中的错误就是实现了error接口的类型,我们可以在函数的返回值中返回一个实现了error接口的值,用于告诉函数的调用者,当前函数是否正常执行,如果没有正常执行,那么错误信息是什么。

怎样处理异常和错误

在Go语言中,处理异常和错误我们一般使用以下的两种方式。

对于错误(error),我们一般是这样处理的:

咳咳,正经的说,因为Go语言中的error是一个接口,所以返回的错误总是一个值,我们可以通过对这个值进行判断来了解当前发生了什么错误,并且知道该如何处理该错误。例如:

go 复制代码
if err == ErrSomething { ... }

在这里,如果当前发生的错误是ErrorSomething我们就执行对该错误的处理,如果是其他错误,也是一样的。但是我们最常用的,还是上面这张图中的代码(每个Gopher都需要这样一个键盘!),因为我们一般不会在当前函数中处理当前函数发生的错误,一般是将错误传播回去,由上层来决定如何处理。

而对于异常,也就是由panic引发的,异常一般会导致程序崩溃,这代表发生了非常严重的错误,如果在开发环境中我们一般不会去处理该错误,而是借助该错误来Debug,如果是线上的运行环境,我们担心发生了panic,那我们一般会使用recover函数来捕获这个panic然后将其当做一个错误传递出去,具体的示例如下:

go 复制代码
package main
import "fmt"
func mayPanic(){
    panic("a problem")
}

func main(){
    defer func(){
        if err:=recover();err!=nil{
            fmt.Println(err)
        }
    }()
    mayPanic()
}

在这段代码中,我们使用了recover来捕获发生的panic然后将其打印出来。在实际的环境中,我们也可以使用这样的方式,来防止程序崩溃。

在Go语言中对于异常和错误的处理,一般情况就是以上的两种方式。

处理异常和错误的注意事项

除了以上的方式,还有一些特殊的情况需要了解,接下来我们来看一个特殊的例子,那就是在defer执行的函数中发生了panic会怎么样呢?示例如下:

go 复制代码
func mayPanic() {
	defer func() {
		fmt.Println("may Panic 1")
	}()
	defer func() {
		panic("I'll panic!")
	}()
	defer func() {
		fmt.Println("may Panic 2")
	}()
	fmt.Println("mayPanic Invoke")
}

func main() {
	defer func() {
		fmt.Println("main function defer invoke")
	}()
	mayPanic()
	fmt.Println("main function invoke!")
}

对于这段代码,运行的结果如下:

我们再来看一个例子,对比着看:

go 复制代码
func mayPanic() {
	defer func() {
		fmt.Println("may Panic 1")
	}()
	defer func() {
		panic("I'll panic!")
	}()
	defer func() {
		fmt.Println("may Panic 2")
	}()
	panic("It's problem")
    defer func(){
        fmt.Println("may Panic 3")
    }()
}

func main() {
	defer func() {
		fmt.Println("main function defer invoke")
	}()
	mayPanic()
	fmt.Println("main function invoke!")
}

这个例子的输出如下:

从以上的两个例子中,我们可以简单的得出一个结论,当一个函数内部发生了panic,如果在发生panic之前,函数内部注册了defer语句,那么panic会被推迟到所有defer语句执行完毕。如果注册了多个defer语句,这多个defer语句中有一个或者多个发生了panic,都不会影响剩下的defer语句执行,只是会将panic的信息记录下来,直到所有defer语句执行完毕,再和函数内部发生的panic一起返回到上层函数,如果上层函数没有捕获,那么该panic就会导致程序崩溃。

至于为什么会发生这种情况,那当然是因为,我们要在defer语句中捕获panic,而编译器不知道哪个defer语句才会捕获,就只能全部执行。如果当前函数中没有注册defer语句,那么该函数就会直接将panic传播到上层。

而对于defer语句中发生的panic,如果需要捕获,那就在defer语句中套娃,示例如下:

go 复制代码
func mayPanic() {
	defer func() {
		fmt.Println("may Panic 1")
	}()
	defer func() {
        defer func(){
            if err:=recover();err!=nil{
                fmt.Println(err)
            }
        }()
        panic("I'll panic!")
	}()
	defer func() {
		fmt.Println("may Panic 2")
	}()
	panic("It's problem")
    defer func(){
        fmt.Println("may Panic 3")
    }()
}

关于panic的内容暂且就这么多,接下来看看关于error的内容,这一部分我们主要了解,除了:

go 复制代码
if err!=nil{
    return nil,err
}

之外的处理方式,比如自定义错误,前面我们说了,在Go语言中,只要一个类型实现了error接口,那么该类型就可以当做一个错误使用,比如:

go 复制代码
type MyError struct{
    name string
    age int
}

func (m *MyError) Error() string {
	return m.name + string(m.age)
}

这样我们就自定义了一个错误, 当我们需要使用的时候,就可以将这个类型,当做一个错误来进行处理。

相关推荐
童先生1 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
幼儿园老大*2 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
架构师那点事儿7 小时前
golang 用unsafe 无所畏惧,但使用不得到会panic
架构·go·掘金技术征文
于顾而言1 天前
【笔记】Go Coding In Go Way
后端·go
qq_172805591 天前
GIN 反向代理功能
后端·golang·go
follycat1 天前
2024强网杯Proxy
网络·学习·网络安全·go
OT.Ter1 天前
【力扣打卡系列】单调栈
算法·leetcode·职场和发展·go·单调栈
探索云原生1 天前
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
ai·云原生·kubernetes·go·gpu
OT.Ter1 天前
【力扣打卡系列】移动零(双指针)
算法·leetcode·职场和发展·go
码财小子2 天前
k8s 集群中 Golang pprof 工具的使用
后端·kubernetes·go