Golang中defer和return的执行顺序 + 相关测试题(面试常考)

简介: Golang中defer和return的执行顺序 + 相关测试题(面试常考)

参考文章:

面试富途的时候,遇到了1.2的这个进阶问题,没回答出来。这种题简直是 $\color{purple}{噩梦}$,很久不关注基本上就忘记了...


一、defer相关测试题

1.1 关于 defer 函数后的 匿名/有名 返回值对输出结果的影响:

package main

import (
    "fmt"
)

func f1() int {
    var i int
    defer func(){
        i++
    }()
    return i    
}

func f2() (i int) {
    defer func(){
        i++
    }()
    return i    
}

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

上面的程序输出结果是啥呢?答案是:0 1

我们知道Golang中的函数返回值有匿名和有名两种

  • 对于匿名的可以理解成执行return 语句时,分成两步,第一步需要设置一个临时变量s用来接收返回值,第二步将临时变量s返回。
  • 对于有名的可以理解成执行return的时候,直接将变量返回。

而我们知道,所有的defer都将在真正的 return 变量之前运行,所以对于上面两种情况,defer对于返回值的影响也有两种:

  • 对于匿名的:第一步设置临时变量保存返回值;第二步按照defer的执行步骤执行defer语句,如果其中有对变量的修改,将不会影响s变量的值。
  • 对于有名的:第一步先执行defer,对变量进行修改;第二步,返回被修改的返回值。

所以,理解了上面的步骤的之后,我们就可以理解 f1()f2() 这两个的函数了,在执行 return 时:

  • f1函数先通过临时变量s保存i的值,此时i为0,所以s=0,然后执行defer,修改i的值,i变为1,最后返回s的值,因为s=0,所以返回值是0;
  • 而f2是有名函数,所以在执行 return 时,执行 i++ 后 i 的值为1,然后真正的返回 i 值,所以返回的是 1。

1.2 关于defer的进阶测试题

再来看一段示例代码:

可以先尝试想想以下的几个函数分别会输出什么内容?

func test1() (x int) {
    defer fmt.Printf("in defer: x = %d\n", x)
    x = 7
    return 9
}
 
func test2() (x int) {
    x = 7
    defer fmt.Printf("in defer: x = %d\n", x)
    return 9
}
 
func test3() (x int) {
    defer func() {
        fmt.Printf("in defer: x = %d\n", x)
    }()
 
    x = 7
    return 9
}
 
func test4() (x int) {
    defer func(n int) {
        fmt.Printf("in defer x as parameter: x = %d\n", n)
        fmt.Printf("in defer x after return: x = %d\n", x)
    }(x)
 
    x = 7
    return 9
}
 
func main() {
    fmt.Println("test1")
    fmt.Printf("in main: x = %d\n", test1())
    fmt.Println("test2")
    fmt.Printf("in main: x = %d\n", test2())
    fmt.Println("test3")
    fmt.Printf("in main: x = %d\n", test3())
    fmt.Println("test4")
    fmt.Printf("in main: x = %d\n", test4())
}

你已经计算出结果了吗?看看和运行结果是不是一样的,如果不一样继续阅读本文吧:

test1
in defer: x = 0
in main: x = 9
test2
in defer: x = 7
in main: x = 9
test3
in defer: x = 9
in main: x = 9
test4
in defer x as parameter: x = 0
in defer x after return: x = 9
in main: x = 9

进阶测试题 解析

在看解析之前,先理解下 return 语句的执行顺序:
return语句本身并不是一条原子指令,它会先给返回值赋值,然后再是真正的返回结果到函数外部,如下:

func f() (r int) {
 return 1
}

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

而在含defer表达式时,函数返回的过程是这样的:
先给返回值赋值,然后调用defer表达式,最后再是返回结果

这4个测试函数中,都是return 9并且没有对返回值进行修改,所以主函数main()中的输出都是:in main: x = 9,我相信这个大家应该是没有疑问的。接下来看每个测试函数defer的打印。

test1:defer执行时,对Printf的入参x进行计算,它的值是0,并且传递给函数,return 9后执行Printf,所以结果是in defer: x = 0。

func test1() (x int) {
    // defer后接【表达式】:
    // 起初x作为入参值为0,结果一直保留在栈中,最后直接打印输出,不受后续影响
    defer fmt.Printf("in defer: x = %d\n", x)
    x = 7
    // 返回值为 9
    return 9
}

test2:与test1类似,不同的是,defer执行是在x=7之后,所以x的值是7,并且传递给Printf,所以结果是:in defer: x = 7。

func test2() (x int) {
    x = 7
    // defer后接【表达式】:
    // 起初x作为入参值为7,结果一直保留在栈中,最后直接打印输出,不受后续影响
    defer fmt.Printf("in defer: x = %d\n", x)
    // 返回值为 9
    return 9
}

test3defer后跟的是一个匿名函数,匿名函数能访问外部函数的变量,这里访问的是test3的x,defer执行时,匿名函数没有入参,所以把func()()压入到栈,return语句之后,执行func()(),此时匿名函数获得x的值是9,所以结果是in defer: x = 9。

func test3() (x int) {
    // defer后接【匿名函数】:
    defer func() {
        // 匿名函数能访问外部函数的变量,受后续return x的结果影响
        // 也就是说【defer匿名函数内要访问的变量 x】最终会被【defer匿名函数外最终要 return 返回出去的变量 x】所影响
        fmt.Printf("in defer: x = %d\n", x)
    }()
 
    x = 7
    // 返回值为 9
    return 9
}

test4:与test3的不同是,匿名函数有一个入参n,我们把x作为入参打印,还有就是匿名函数中的x能访问外部函数的变量x。defer执行时,x=0,所以入栈的函数是func(int)(0),return语句之后执行func(int)(0),即n=0,x在匿名函数内没有定义,依然访问test4中的x,此时x=9,所以结果为:in defer x as parameter: x = 0, in defer x after return: x = 9。

func test4() (x int) {
    // defer后接【匿名函数】:
    defer func(n int) {
        // 针对【defer匿名函数内要访问的变量 n】,其值取决于在一开始遇到 defer 时入参 n 的值,
        // 也就是最开始还没被改变时的变量 x 的值,将其赋值给入参 n,起初是0
        // 这个变量 n 的值是独立的,不会受后续 return x 结果的影响。
        fmt.Printf("in defer x as parameter: x = %d\n", n)
        fmt.Printf("in defer x after return: x = %d\n", x)
    }(x)
 
    x = 7
    return 9
}

小结:

  • 首先要明确 defer 后紧接的代码可以有两种写法:

    • defer + 表达式,例如:defer fmt.Printf("in defer: x = %d\n", x)

    此时就会直接保留当前变量 x 已有的值到栈中,一直到最后直接打印输出,不再受后续 return x 结果的影响。$\color{red}{例如 test1 和 test2 中的变量 x。}$

    • defer + 匿名函数(无入参/有入参),例如:test3中的 defer func() {fmt.Printf("in defer: x = %d\n", x)}()或 test4中 defer func(n int) {fmt.Printf("in defer x as parameter: x = %d\n", n) fmt.Printf("in defer x after return: x = %d\n", x)}(x)

    此时需要区分打印输出的变量到底是【defer匿名函数内要访问的变量 n】,还是【defer匿名函数内要访问的变量 x】。

    - 针对【defer匿名函数内要访问的变量 **n**】,其值取决于在一开始遇到 defer 时入参 n 的值(也就是最开始还没被改变时的变量 x 的值,起初是0)。这个变量 n 的值是独立的,不会受后续 return x 结果的影响。$\color{red}{例如 test4 中的变量 n。}$
    - 针对【defer匿名函数内要访问的变量 **x**】,由于**匿名函数能访问外部函数的变量**,也就是说【defer匿名函数内要访问的变量 x】最终会被【defer匿名函数外最终要 return 返回出去的变量 x】所影响。$\color{red}{例如 test3 和 test4 中的变量 x。}$
    
目录
相关文章
|
3月前
|
运维 测试技术
拆分软件测试流程,一张图秒杀所有面试
本文主要介绍了软件测试流程的核心内容,包括需求分析、测试用例编写、测试执行、缺陷提交及回归测试等关键步骤。以迭代测试为例,详细说明了每个环节的具体操作和注意事项,并提供了一张测试流程图以便理解。测试流程确保了软件质量,是面试中常见的考察点。
74 7
拆分软件测试流程,一张图秒杀所有面试
|
2月前
|
Go Python
通过 atexit 模块让 Python 实现 Golang 的 defer 功能
通过 atexit 模块让 Python 实现 Golang 的 defer 功能
25 2
|
4月前
|
测试技术 Go
写出高质量代码的秘诀:Golang中的测试驱动开发(TDD)
写出高质量代码的秘诀:Golang中的测试驱动开发(TDD)
|
4月前
|
SQL 安全 测试技术
[go 面试] 接口测试的方法与技巧
[go 面试] 接口测试的方法与技巧
|
4月前
|
测试技术 Go 开发者
掌握Golang测试:从入门到实践
【8月更文挑战第31天】
68 0
|
5月前
|
运维 监控 测试技术
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
Golang质量生态建设问题之接入并使用Go单元测试插件的问题如何解决
|
5月前
|
测试技术 Shell Go
Golang质量生态建设问题之单元测试在卓越工程中的问题如何解决
Golang质量生态建设问题之单元测试在卓越工程中的问题如何解决
|
4月前
|
自然语言处理 网络协议 JavaScript
23.2月 可能七牛云实习测试面试(技术面一面)面经整理
关于2月进行的七牛云实习测试面试(技术面一面)的面经整理,涵盖了多个技术问题,包括马尔可夫链的用处、软件测试工具、TCP/IP协议的三次握手过程、TCP与UDP的区别、网络诊断方法、DNS的作用、ifconfig命令的用途、Spring Boot的优势以及Java中Map的了解,还包括了一个编程题目:在n个书中找出k个最小的数。
|
4月前
|
Serverless Go
Golang 开发函数计算问题之defer 中的 recover() 没有捕获到 如何解决
Golang 开发函数计算问题之defer 中的 recover() 没有捕获到 如何解决
|
4月前
|
人工智能 数据库连接 Go
golang defer 详解
golang defer 详解
53 0
下一篇
DataWorks