Go面试题:聊聊你理解的Golang defer关键字

面试题文档下链接点击这里免积分下载

文章目录

defer两大特性

defer是golang中的一个关键字,它主要具有两大特性:

  • 延迟调用: 在当前函数执行完成后调用执行。
python 复制代码
func f1(){
	defer fmt.Println("hello world")

	fmt.Println("hello defer!")
}

输出结果:

python 复制代码
$ go run main.go
hello defer!
hello world
  • 后进先出: 多个defer函数时,执行顺序为后进先出。
python 复制代码
func f2(){
	defer fmt.Println("hello 1!")
	defer fmt.Println("hello 2!")
	defer fmt.Println("hello 3!")

	fmt.Println("hello defer!")
}

输出结果

python 复制代码
$ go run main.go
hello defer!
hello 3!
hello 2!
hello 1!

defer与return的执行顺序

defer与return的执行顺序,是面试时经常考察的一点,需要道友们好好理解。

首先,我们举个栗子,看如下情况下代码的输出结果。

python 复制代码
func f1() (r int){
	defer func(){
		r++
	}()
	return 0
}

func f2() (r int) {
	t:=5
	defer func() {
		t = t+5
	}()
	return t
}

func f3() (r int) {
	defer func(r int) {
		r = r+5
	}(r)
	return 0
}

func main(){
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
}

建议朋友们先思考下答案,再往后看。

python 复制代码
$ go run main.go
1
5
0

好的,下面我们逐一分析一下:

这里我们需要先理解下return语句的执行顺序。

return语句本身并不是一条原子指令,它会先给返回值赋值,然后再是返回,如下

python 复制代码
func f4() (r int) {
	return 1
}

//执行过程:
r:=1 //赋值
ret //执行返回

而在含defer表达式时,函数返回的过程是这样的:

先给返回值赋值,然后调用defer表达式,最后再是返回结果

即对于f1()来讲

python 复制代码
func f1() (r int){
	defer func(){
		r++
	}()
	return 0
}

//执行过程:
r:=0 //赋值
r++  //defer
ret  //r=1

对于f2来讲

python 复制代码
func f2() (r int) {
	t:=5
	defer func() {
		t = t+5
	}()
	return t
}

//执行过程
t:=5
r:=t
t=t+5 //defer
ret  //r=5

对于f3()来讲,在defer的时候传参r,其实是一个值拷贝。

所以defer中对r的修改并不会影响返回值结果,助于理解把r换成t,结果是等同的,即等效为

python 复制代码
func f3() (r int) {
	defer func(t int) {
		t = t+5
	}(r)
	return 0
}

//执行过程
r:=0
t = r, t = t +5 //defer
ret // r=0

defer的应用场景

场景一:资源释放

我们在代码中使用资源时如:打开一个文件,很容易因为忘记释放或者由于逻辑上的错误导致资源没有关闭。这时候使用defer可以避免这种资源泄漏。不妨先看如下代码:

python 复制代码
file,_ := os.Open("test.txt")
//process为业务逻辑处理
if err:=process(file);err!=nil {
  return
}
file.Close()

上面的代码即存在一个严重的问题,如 err!=nil 直接return后,会使得file.close() 关闭资源的语句没有执行,导致资源泄漏。

且在经历了一串业务逻辑处理编写后,我们也很容易忘记关闭资源导致资源泄漏。所以应该牢记一个原则:在每个资源申请成功的后面都加上defer自动清理,不管该函数都多少个return,资源都会被正确的释放。
正确的编写逻辑如下:

python 复制代码
file,_ := os.Open("test.txt")
defer file.Close()
//process为业务逻辑处理
if err:=process(file);err!=nil {
  return
}

场景二:异常捕获

Golang中对于程序中的异常处理,没有try catch,但是有panic和recover。 当程序中抛出panic时,如果没有及时recover,会导致服务直接挂掉,造成很严重的后果,所以我们一般用recover来捕获异常。

python 复制代码
func main(){
	defer func(){
		if ok:=recover();ok!=nil{
			fmt.Println("recover")
		}
  }()
	panic("error")
}

上面两个场景是我们必需要熟知的,当然还可以利用defer的特性优雅的实现一些类似于代码追踪、记录函数的参数和返回值等。
场景三: 代码追踪

我们通过追踪程序进入或离开某个函数的信息,来测试此函数是否被执行。

python 复制代码
func main(){
	f1()
	f2()
}

func f1(){
	defer trace_leave(trace_enter("f1()"))
	fmt.Println("f1()程序逻辑")
}

func f2(){
	defer trace_leave(trace_enter("f2()"))
	fmt.Println("f2()程序逻辑")
}

func trace_enter(msg string) string{
	fmt.Println("enter: ",msg)
	return msg
}

func trace_leave(msg string) {
	fmt.Println("leave: ",msg)
}

输出结果如下:

python 复制代码
$go run main.go
enter:  f1()
f1()程序逻辑
leave:  f1()
enter:  f2()
f2()程序逻辑
leave:  f2()

场景四: 打印函数的参数和返回值

某函数的执行结果不符合预期,我们可以使用defer来一步到位的打印函数的参数和返回值,而非多处打印调试语句。

python 复制代码
func main(){
	func1("hello")
}

func func1(str string) ( res string) {
	defer func() {
		fmt.Printf("func1(%s) = %s", str, res)
	}()
	res = fmt.Sprintf("%s, jack!",str)
	return
}

输出结果:

python 复制代码
$go run main.go
func1(hello) = hello, jack!

面试点总结

  • defer的两大特性
  • defer与return的执行顺序
  • defer的应用场景
相关推荐
西猫雷婶2 小时前
python学opencv|读取图像(十九)使用cv2.rectangle()绘制矩形
开发语言·python·opencv
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
海绵波波1072 小时前
flask后端开发(10):问答平台项目结构搭建
后端·python·flask
码农W2 小时前
QT--静态插件、动态插件
开发语言·qt
ke_wu3 小时前
结构型设计模式
开发语言·设计模式·组合模式·简单工厂模式·工厂方法模式·抽象工厂模式·装饰器模式
code04号3 小时前
python脚本:批量提取excel数据
开发语言·python·excel
小王爱吃月亮糖3 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
hakesashou3 小时前
python如何打乱list
开发语言·python
网络风云3 小时前
【魅力golang】之-反射
开发语言·后端·golang
Q_19284999064 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端