更新于 

defer&命名返回值和非命名返回值的区别

结论

首先 return 非原子返回的这个结论之前已经说过了,然后我们看下在使用 defer 对返回值进行修改时,针对命名返回值和非命名返回值,有啥不同,先说结论

  • 命名返回值本质上是函数作用域内的一个变量,defer 可以修改它;而非命名返回值是通过值复制的方式传出,defer 修改不了。

名词解释

  • 函数的栈帧(stack frame)是函数在运行时在栈(stack)上分配的一块内存,用来保存:参数,返回值,局部变量,返回地址,可能还有调用者的一些上下文信息(如寄存器、defer 信息等)

解释

通俗来讲,就是变量的作用域不一致,可以理解为,🎯 有没有一个“命名的栈变量”供 defer 修改

命名返回值就是一个在函数作用域里声明的变量,会在栈帧中提前分配空间,defer defer 可以直接访问和修改它。重点:函数有命名返回值 result,这个返回值会被存储在栈帧中。Go 会为 result 分配一个局部变量,并将其初始化为零值(在下面个例子中是 int 类型,初始化值为 0)。

非命名返回值是一个匿名的 返回值槽,return 时只是把局部变量的值复制进去,defer 修改不了。

示例

  • 示例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func demo1() (result int) {
// 1. 命名返回值可以理解为 var result int
defer func() {
result = 2 // 3. 改的是函数的返回变量 result
fmt.Println("Defer executed")
}()
result = 1 // 2. 更新命名返回值 result int 的值为 1
return result // 4. 此时返回的变量就是 result int 命名返回值里的变量
}

func main() {
fmt.Println(demo1()) // 输出 2
}
  • 示例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func demo2() int {
result := 1 // 1. 定义局部变量 result 为 int类型 并初始化值为 1
defer func() {
result = 2 // 3. 改的是函数的返回变量 result
fmt.Println("Defer executed")
}()
// 因为 return 并非原子操作,所以 return result 可以拆分为
// 2. return_res = result,当然 return_res 可以是任意变量名称,不影响结果,这里有个 `返回值槽`( `返回寄存器` `返回区域`) 的概念
// 3. 这里要执行的是defer函数,那我们回到第三步 defer函数体内部 result = 2,此时修改的 result 变量的值 为2 ,但是在这个之前 return_res 值已经确定了,所以即使 result 变量已经改变了,也不会影响 第四步的返回值
// 4. return return_res
return result // 4. 此时返回的变量就是 return_res 的值
}

func main() {
fmt.Println(demo2()) // 输出 1
}

🌟 什么是“返回值槽”?

  • 可以理解成:函数在返回之前,把值放进一个“临时的传送带槽位”里,调用者会从这个槽中取回返回值。
    非命名返回值,这个“槽”只是个临时复制品,defer 改不了。
    命名返回值,这个“槽”就是函数作用域内的变量,defer 可以改它。

示例

  • 接下来我们引入返回值槽的概念,对上面两个函数做进一步拆解

  • 示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func demo1_solt() (result int) {
result = 1
defer func() {
result = 2 // 直接修改 result,就修改了返回槽本身
fmt.Println("Defer executed")
}()
return result // 在这里,result 本身就是返回槽的别名,所有对它的修改,都会直接影响最终返回的值。
}

func main() {
fmt.Println(demo1_solt()) // 输出 2
}
  • 示例2

流程图

流程图
流程图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func demo2_solt() int {
result := 1 // 1. var result int = 1
// runtime 会创建一个“返回值槽”,比如叫 _retSlot
// var _retSlot int
defer func() {
result = 2 // 修改的是局部变量 result,不影响 _retSlot
fmt.Println("Defer executed")
}()
// 因为 return 并非原子操作,所以 return result 可以拆分为
// 2. _retSlot = result,这里 `_retSlot` 就是返回值槽变量名
// 3. 这里要执行的是defer函数,那我们回到第三步 defer函数体内部 result = 2,此时修改的 result 变量的值 为2 ,但是在这个之前 _retSlot 值已经确定了,所以即使 result 变量已经改变了,也不会影响 第四步的返回值
// 4. return _retSlot
return result // 4. 此时返回的变量就是 _retSlot 的值
}

func main() {
fmt.Println(demo2_solt()) // 输出 1
}