这是「前端开发转 Go 全栈」系列的第四篇。
上一篇:前端开发转 Go 全栈(三):从函数到 error,Go 连"失败"都要明确返回
上一篇从函数定义、参数与多返回值,一路学习到了
error、错误包装和错误传递。这一篇继续学习函数相关内容,重点弄清楚命名返回值、裸返回,以及 Go 中非常有特点的defer。
前言:函数执行到 return,真的结束了吗?
上一篇学习函数和错误处理时,我经常写这样的代码:
go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
函数通过:
go
return a / b, nil
明确返回计算结果和错误。
按照以前写 JavaScript 的习惯,我一直觉得:
函数执行到
return,这次调用就已经结束了。
但这次学习 defer 后,我发现 Go 中的 return 并不一定意味着函数已经真正退出。
在函数返回之前,可能还有一些提前登记好的操作需要执行:
text
计算并设置返回值
→ 执行 defer
→ 函数真正返回
更反直觉的是:
函数已经执行到
return了,defer居然还能修改最终返回值。
这一次实际学习和验证的内容主要有两个:
text
命名返回值
defer
命名返回值还比较容易理解,无非是提前给函数的返回结果起个名字。
但 defer 就有点反直觉了。
代码明明写在前面,却不会立即执行,而是要等当前函数准备结束时才运行。
作为一名前端开发,我看到 defer 的第一反应是:
arduino
text
它是不是有点像 setTimeout?
但真正写完几个实验后,我发现它们完全不是一回事。
这篇文章就来记录一下,我是怎么一步步弄清楚命名返回值、裸返回和 defer 的。
返回值也可以提前起名字
上一篇写过一个除法函数:
go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
这里的返回值只有类型:
go
(int, error)
Go 还允许我们直接给返回值起名字:
go
func divide(a, b int) (result int, err error)
完整代码如下:
go
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (result int, err error) {
if b == 0 {
err = errors.New("除数不能为 0")
return
}
result = a / b
return
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("计算失败:", err)
return
}
fmt.Println("计算结果:", result)
}
运行结果:
text
计算结果: 5
再把除数改成 0:
go
result, err := divide(10, 0)
输出:
text
计算失败: 除数不能为 0
命名返回值进入函数时就已经存在
重点看函数声明:
go
func divide(a, b int) (result int, err error)
这里提前声明了两个返回值:
text
result int
err error
可以暂时理解为,函数刚开始执行时,Go 已经准备好了两个变量:
go
var result int
var err error
而且它们已经获得了对应类型的零值:
text
result = 0
err = nil
所以成功时,只需要给 result 赋值:
go
result = a / b
return
失败时,只需要给 err 赋值:
go
err = errors.New("除数不能为 0")
return
函数中的其他代码也可以直接读取和修改这些命名返回值。
return 后面什么都不写,也能返回?
前面的代码里出现了一个看起来有点奇怪的写法:
go
return
之前的 return 都会明确写出返回内容:
go
return a / b, nil
但是使用命名返回值后,只写一个 return 也可以。
因为函数已经提前声明了:
go
(result int, err error)
Go 会返回当前的 result 和 err。
例如:
go
result = a / b
return
可以暂时理解成:
go
return result, err
这种没有在 return 后明确写出返回表达式的方式,通常叫作裸返回。
命名返回值不代表必须裸返回
即使返回值已经有名字,仍然可以显式返回。
例如:
go
func divide(a, b int) (result int, err error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
也就是说,下面两种写法都可以。
使用裸返回
go
func divide(a, b int) (result int, err error) {
if b == 0 {
err = errors.New("除数不能为 0")
return
}
result = a / b
return
}
使用显式返回
go
func divide(a, b int) (result int, err error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
裸返回确实可以少写几个字符。
但函数一旦变长,问题也会随之出现。
看到:
go
return
很容易产生疑问:
text
当前的 result 是什么?
err 有没有在某个分支中被修改?
这个 return 最终返回了哪些值?
所以我目前的理解是:
命名返回值需要认识,裸返回也需要看得懂,但不能为了少写几个字就在所有函数中使用。
在很短的函数中,裸返回可能还比较清晰。
函数稍微复杂一些,明确写出返回内容通常更容易阅读:
go
return result, err
什么都不赋值,会返回什么?
为了验证命名返回值的初始值,我又写了一个函数:
go
package main
import "fmt"
func getDefaultValues() (count int, message string, success bool) {
return
}
func main() {
count, message, success := getDefaultValues()
fmt.Printf("count:%d\n", count)
fmt.Printf("message:%q\n", message)
fmt.Printf("success:%t\n", success)
}
运行结果:
text
count:0
message:""
success:false
虽然函数体中什么都没有做,但三个返回值依然存在:
text
int → 0
string → ""
bool → false
这再次说明:
命名返回值在函数开始执行时就已经存在,并自动获得对应类型的零值。
因此,下面的函数会直接返回 0:
go
func getCount() (count int) {
return
}
但这种写法虽然合法,表达的意图并不一定清楚。
如果函数本来就是要返回 0,直接写出来通常更直观:
go
func getCount() int {
return 0
}
从前端视角理解命名返回值
JavaScript 和 TypeScript 没有与 Go 命名返回值完全对应的语法。
TypeScript 通常只能声明整个返回结果的类型:
typescript
function divide(
a: number,
b: number
): { result: number; error: Error | null } {
if (b === 0) {
return {
result: 0,
error: new Error("除数不能为 0"),
};
}
return {
result: a / b,
error: null,
};
}
这里的 result 和 error 是返回对象的属性。
Go 的命名返回值则直接属于函数声明:
go
func divide(a, b int) (result int, err error)
它们在进入函数时,就已经是可以访问和修改的变量:
go
result = a / b
err = nil
这也是后面 defer 能够修改最终返回值的重要前提。
defer:代码先登记,函数结束前再执行
接下来进入这篇真正反直觉的部分。
先看第一段代码:
go
package main
import "fmt"
func study() {
fmt.Println("1. 开始学习")
defer fmt.Println("3. 学习结束")
fmt.Println("2. 正在学习")
}
func main() {
study()
}
运行结果:
text
1. 开始学习
2. 正在学习
3. 学习结束
代码中的顺序明明是:
text
1
3
2
输出却是:
text
1
2
3
原因是:
go
defer fmt.Println("3. 学习结束")
不会立即调用 fmt.Println。
它会先登记这次延迟调用,等当前函数准备退出时再执行。defer 后面的调用会在包含它的函数即将返回前运行。
实际过程可以理解为:
text
输出"开始学习"
→ 登记 defer
→ 输出"正在学习"
→ study 准备结束
→ 执行 defer
→ study 真正结束
这里需要特别注意:
defer关注的是当前函数,不是整个程序。
上面的 defer 写在 study 中,所以它会在 study 退出前执行,而不是等到 main 结束时再执行。
defer 不是 setTimeout
作为前端开发,看到"延迟执行",很容易想到:
javascript
setTimeout(() => {
console.log("稍后执行");
}, 0);
但 defer 和 setTimeout 完全不是一回事。
text
setTimeout
→ 把回调交给定时器和事件循环机制
→ 等满足条件后再进入任务队列
→ 属于 JavaScript 异步调度的一部分
而:
text
defer
→ 仍然属于当前函数的执行流程
→ 在当前函数退出前同步执行
→ defer 执行完成后,当前函数才会真正返回
所以:
text
defer ≠ setTimeout
defer ≠ 异步任务
defer ≠ 开启新的 goroutine
defer 只是把一次函数或方法调用安排到当前函数退出前执行。
JavaScript 中更接近 defer 的是 finally
看下面这个 Go 函数:
go
package main
import "fmt"
func study(success bool) {
fmt.Println("开始执行 study")
defer fmt.Println("study 即将结束")
if !success {
fmt.Println("学习失败,提前返回")
return
}
fmt.Println("学习成功")
}
func main() {
study(false)
}
输出:
text
开始执行 study
学习失败,提前返回
study 即将结束
即使函数提前执行了:
go
return
之前已经登记的 defer 仍然会执行。
JavaScript 中比较接近的写法是:
javascript
function study(success) {
console.log("开始执行 study");
try {
if (!success) {
console.log("学习失败,提前返回");
return;
}
console.log("学习成功");
} finally {
console.log("study 即将结束");
}
}
study(false);
输出同样是:
text
开始执行 study
学习失败,提前返回
study 即将结束
两者表达的意图比较接近:
text
Go defer
→ 当前函数退出前执行收尾操作
JavaScript finally
→ try 代码块退出时执行收尾操作
不过两者并不完全等价。
Go 可以在函数的不同位置登记多个 defer,不需要把主要逻辑全部放进一个 try 代码块。
所以可以使用 finally 帮助建立初步理解,但不能直接把它们当成完全相同的语法。
为什么 defer 适合做资源清理?
假设以后需要打开一个文件:
go
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close()
这里可以理解为:
text
文件成功打开
→ 立即登记"函数退出前关闭文件"
→ 中间继续处理文件
→ 即使某个分支提前 return
→ 最终仍然会调用 file.Close()
以后还会看到类似代码:
go
defer rows.Close()
go
defer response.Body.Close()
go
defer mutex.Unlock()
它们都有一个共同点:
text
前面获得了某种资源
→ 当前函数结束前需要释放
把清理操作紧跟在成功获得资源的代码后面,可以减少后续增加多个返回分支时遗漏清理逻辑的可能。defer 也经常被用于文件关闭、资源释放和锁的解锁。
现在我还没有正式学习文件、数据库和锁,但已经可以先理解为什么 defer 会频繁出现在这些场景中。
需要注意的是:
必须先确认资源获取成功,再登记对应的清理操作。
也就是先判断 err:
go
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close()
而不是不管文件有没有打开成功,就直接尝试关闭。
多个 defer,谁先执行?
接下来,我又登记了三个 defer:
go
package main
import "fmt"
func main() {
defer fmt.Println("第一个 defer")
defer fmt.Println("第二个 defer")
defer fmt.Println("第三个 defer")
fmt.Println("main 正在执行")
}
运行结果:
text
main 正在执行
第三个 defer
第二个 defer
第一个 defer
多个 defer 不是按照登记顺序执行,而是完全反过来。
它们遵循:
text
后进先出
也就是:
text
先登记第一个
→ 再登记第二个
→ 最后登记第三个
执行时:
第三个
→ 第二个
→ 第一个
这种顺序也叫:
text
LIFO
Last In, First Out
多个 defer 会按照后进先出的顺序运行,最后登记的延迟调用最先执行。
可以把它想象成叠盘子:
text
第一个盘子放在最下面
第二个盘子放在中间
第三个盘子放在最上面
取盘子时:
先取第三个
再取第二个
最后取第一个
这种执行顺序对于多个资源的清理也很有意义。
假设资源是按照下面的顺序获得的:
text
获得资源 A
→ 获得资源 B
→ 获得资源 C
清理时通常会反过来:
text
释放资源 C
→ 释放资源 B
→ 释放资源 A
变量修改后,defer 输出旧值还是新值?
接着,我测试了一个很容易猜错的问题:
go
package main
import "fmt"
func main() {
name := "JavaScript"
defer fmt.Println(name)
name = "Go"
}
最后输出的是:
text
JavaScript
而不是:
text
Go
原因是:
defer延迟的是函数调用,但函数参数会在执行到defer这一行时立即求值并保存。
执行到:
go
defer fmt.Println(name)
时,name 的值还是:
text
JavaScript
为了帮助自己理解,可以近似想象成 Go 先做了:
go
savedName := name
然后登记:
go
defer fmt.Println(savedName)
后面虽然执行:
go
name = "Go"
但是已经确定的参数值不会跟着变化。
所以需要区分两个时间点:
text
defer 后面的函数调用
→ 当前函数退出前执行
传给这次调用的参数
→ 执行到 defer 语句时确定
可以总结为:
调用延迟,参数不延迟。
直接传参和匿名函数读取变量并不一样
前面的写法是直接把 name 作为参数传给 fmt.Println:
go
defer fmt.Println(name)
如果改成匿名函数:
go
package main
import "fmt"
func main() {
name := "JavaScript"
defer func() {
fmt.Println(name)
}()
name = "Go"
}
这次会输出:
text
Go
原因是,匿名函数本身没有接收 name 参数:
go
func() {
fmt.Println(name)
}()
fmt.Println(name) 位于匿名函数体内,要等这个匿名函数真正执行时才会读取 name。直接作为延迟调用参数传入的表达式会在 defer 语句执行时求值,而匿名函数体内的表达式会在匿名函数真正执行时求值。
二者可以先这样区分:
go
defer fmt.Println(name)
text
name 作为调用参数
→ 执行到 defer 时确定
→ 输出登记时的值
而:
go
defer func() {
fmt.Println(name)
}()
text
name 在匿名函数体内读取
→ 匿名函数实际执行时再读取
→ 可能输出修改后的值
这篇暂时不会单独展开匿名函数和闭包。
这里只需要先意识到:
defer语句执行时会立即计算它的调用参数,但这不等于为整个匿名函数体创建了一份变量快照。
从前端视角理解 defer 参数
JavaScript 可以用下面的代码模拟这种"先保存旧值,最后再使用"的效果:
javascript
function main() {
let name = "JavaScript";
const savedName = name;
try {
name = "Go";
console.log("main 中:", name);
} finally {
console.log("最后输出:", savedName);
}
}
main();
输出:
text
main 中: Go
最后输出: JavaScript
这里真正关键的不是 finally,而是:
javascript
const savedName = name;
在变量修改前,先保存了一份旧值。
Go 的:
go
defer fmt.Println(name)
也会在登记时确定参数值。
所以这个 JavaScript 示例只是帮助理解下面这件事:
text
变量之后还可以修改
但 defer 已经保存了调用时需要的参数值
defer 可以修改命名返回值吗?
接下来进入这次学习中最有意思的实验:
go
package main
import "fmt"
func getNumber() (result int) {
result = 10
defer func() {
result++
fmt.Println("defer 中的 result:", result)
}()
fmt.Println("return 前的 result:", result)
return
}
func main() {
number := getNumber()
fmt.Println("最终返回值:", number)
}
运行结果:
text
return 前的 result: 10
defer 中的 result: 11
最终返回值: 11
函数原本准备返回:
text
10
但是在函数真正退出之前,defer 又把命名返回值增加了 1:
go
result++
最后调用者拿到的结果变成了:
text
11
命名返回值属于当前函数作用域中的变量,而延迟函数会在函数真正返回前执行,因此延迟函数可以访问并修改命名返回值。
return 10 也会被 defer 修改
接着把函数改成:
go
func getNumber() (result int) {
defer func() {
result++
fmt.Println("defer 中的 result:", result)
}()
return 10
}
完整调用:
go
package main
import "fmt"
func getNumber() (result int) {
defer func() {
result++
fmt.Println("defer 中的 result:", result)
}()
return 10
}
func main() {
number := getNumber()
fmt.Println("最终返回值:", number)
}
运行结果仍然是:
text
defer 中的 result: 11
最终返回值: 11
虽然代码明确写了:
go
return 10
但对于命名返回值,可以暂时把这个过程理解为:
go
result = 10
然后执行已经登记的 defer:
go
result++
最后才真正返回。
所以 Go 的 return 并不是看到以后立即退出函数。
它大致会经历三个步骤:
text
1. 计算返回表达式,并设置返回值
2. 执行已经登记的 defer
3. 函数真正退出,把结果交给调用者
因为这里的 result 是命名返回值,所以 defer 修改的正是函数最终要返回的那个变量。
普通返回值为什么没有被修改?
再把函数改成普通返回值:
go
package main
import "fmt"
func getNumber() int {
result := 10
defer func() {
result++
fmt.Println("defer 中的 result:", result)
}()
return result
}
func main() {
number := getNumber()
fmt.Println("最终返回值:", number)
}
运行结果:
text
defer 中的 result: 11
最终返回值: 10
defer 明明把 result 改成了 11,最终返回值为什么还是 10?
因为执行:
go
return result
时,要返回的值已经根据 result 当时的内容计算并保存:
text
准备返回的值 = 10
后面的 defer 修改的是函数中的局部变量:
text
局部变量 result = 11
但是已经准备好的返回结果不会跟着这个局部变量一起变化。
这里的 result 只是一个普通局部变量:
go
result := 10
它并不是命名返回值。
因此:
text
局部变量 result
和:
text
函数准备返回的值
已经不是同一个可以继续共同变化的变量。
三种情况放在一起看
为了更清楚地理解,我把三种情况放在一起比较。
第一种:命名返回值加裸返回
go
func getNumber() (result int) {
result = 10
defer func() {
result++
}()
return
}
最终结果:
text
11
执行过程:
text
result = 10
→ 遇到 return
→ 执行 defer
→ result 变成 11
→ 返回 11
第二种:命名返回值加 return 10
go
func getNumber() (result int) {
defer func() {
result++
}()
return 10
}
最终结果:
text
11
执行过程:
text
把 10 设置给命名返回值 result
→ 执行 defer
→ result 变成 11
→ 返回 11
第三种:普通返回值
go
func getNumber() int {
result := 10
defer func() {
result++
}()
return result
}
最终结果:
text
10
执行过程:
text
计算 return result
→ 准备返回 10
→ 执行 defer
→ 局部变量 result 变成 11
→ 已经准备好的返回值仍然是 10
→ 返回 10
核心差异是:
text
命名返回值
→ defer 修改的是函数最终返回的变量
而:
text
普通局部变量
→ return 表达式已经先计算完成
→ defer 只能修改局部变量
→ 不影响已经准备好的返回结果
可以整理成表格:
表格
| 函数形式 | defer 修改的内容 |
最终结果 |
|---|---|---|
| 命名返回值加裸返回 | 命名返回变量 | 会影响最终返回值 |
| 命名返回值加显式返回 | 命名返回变量 | 会影响最终返回值 |
| 普通返回值 | 普通局部变量 | 不影响已经准备好的返回值 |
JavaScript 的 try/finally 也有类似表现
JavaScript 代码:
javascript
function getNumber() {
let result = 10;
try {
return result;
} finally {
result++;
console.log("finally 中的 result:", result);
}
}
console.log("最终返回值:", getNumber());
输出:
text
finally 中的 result: 11
最终返回值: 10
这和 Go 的普通返回值比较接近:
go
func getNumber() int {
result := 10
defer func() {
result++
}()
return result
}
两者都可以这样理解:
text
先计算 return 表达式
→ 再执行 finally 或 defer
→ 修改局部变量
→ 不影响已经准备好的返回结果
不过 JavaScript 还可以在 finally 中再次执行 return:
javascript
function getNumber() {
try {
return 10;
} finally {
return 11;
}
}
最后会返回:
text
11
因为 finally 中的新 return 覆盖了前面的返回结果。
这种写法非常容易让代码难以理解,实际开发中通常不建议使用。
Go 中的情况稍有不同。
defer 并不是重新执行一次 return,而是在函数真正退出之前,修改了函数的命名返回变量。
defer 修改返回值,真的应该这样写吗?
下面这段代码完全合法:
go
func calculate() (result int) {
defer func() {
result++
}()
return 10
}
但是读代码的人看到:
go
return 10
第一反应大概率是:
text
这个函数返回 10
实际上却返回:
text
11
这会增加阅读和排查成本。
所以我的理解是:
defer可以修改命名返回值,但不代表普通业务代码都应该这样写。
需要理解这种执行机制,是因为以后阅读第三方代码时可能遇到。
但自己编写业务逻辑时,还是应该优先保证返回结果直观。
例如:
go
return result, nil
通常比在 defer 中悄悄修改结果更容易理解和维护。
当然,defer 修改命名返回值也不是完全没有合理场景。
例如,以后可能会看到某些函数在延迟关闭资源时,需要把关闭失败补充到命名错误返回值中。
但在还没有真正遇到这种需求之前,没有必要为了使用 defer 而故意设计复杂的返回逻辑。
return 与 defer 的执行过程
经过前面的实验,我现在会把包含 defer 的返回过程理解成下面这样:
text
函数执行普通代码
→ 遇到 return
→ 计算返回表达式
→ 设置返回值
→ 按后进先出顺序执行所有 defer
→ 函数真正返回
例如:
go
func getNumber() (result int) {
defer func() {
result++
}()
return 10
}
可以近似拆成:
text
result = 10
→ 执行 defer
→ result++
→ result 变成 11
→ 返回 11
而普通返回值:
go
func getNumber() int {
result := 10
defer func() {
result++
}()
return result
}
可以近似拆成:
text
计算 return result
→ 准备返回值 10
→ 执行 defer
→ 局部变量 result 变成 11
→ 返回之前准备好的 10
这些步骤是为了帮助自己理解执行顺序,并不是说编译器一定会把代码逐字改写成上述形式。
真正需要记住的是:
延迟调用会在返回值设置之后、函数真正返回之前执行,并且可以修改仍然可访问的命名返回值。
这一阶段最容易混淆的地方
1. 裸 return 返回什么?
go
func getValue() (result int) {
return
}
返回的是命名返回值当前的内容。
如果没有进行任何赋值,就是对应类型的零值。
上面的函数会返回:
text
0
2. 使用命名返回值后,必须裸返回吗?
不需要。
下面两种写法都合法:
go
func getValue() (result int) {
result = 10
return
}
go
func getValue() (result int) {
return 10
}
命名返回值和裸返回是两个相关但不同的概念:
text
命名返回值
→ 给函数返回结果起名字
裸返回
→ return 后面不写返回表达式
3. defer 是异步的吗?
不是。
text
defer ≠ setTimeout
defer ≠ Promise
defer ≠ goroutine
它仍然属于当前函数的同步执行流程,只是被安排在函数退出前执行。
当前函数需要等所有已登记的 defer 执行完成,才会真正把控制权交还给调用者。
4. return 后,defer 还会执行吗?
会。
执行顺序大致是:
text
计算并设置返回值
→ 执行 defer
→ 真正退出函数
前提是程序已经执行到了对应的 defer 语句,那次延迟调用已经成功登记。
如果某个 defer 写在提前返回的代码之后:
go
func study() {
return
// 无法执行到这里,也就不会登记
defer fmt.Println("不会执行")
}
那么它当然不会运行。
5. 多个 defer 的顺序是什么?
后进先出:
text
最后登记的最先执行
例如:
go
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
执行顺序为:
text
C
B
A
6. defer 的参数什么时候确定?
执行到 defer 这一行时就确定。
go
name := "JavaScript"
defer fmt.Println(name)
name = "Go"
最后输出:
text
JavaScript
因为 name 作为 fmt.Println 的参数,在执行 defer 语句时就已经求值。
7. 匿名函数中的变量也是立即确定吗?
不一定。
例如:
go
name := "JavaScript"
defer func() {
fmt.Println(name)
}()
name = "Go"
输出:
text
Go
这里 name 不是延迟调用的参数,而是在匿名函数真正执行时从函数体中读取。
如果希望明确保存登记时的值,可以把它作为参数传入:
go
name := "JavaScript"
defer func(savedName string) {
fmt.Println(savedName)
}(name)
name = "Go"
这次会输出:
text
JavaScript
因为调用参数 name 会在执行到 defer 语句时求值。
8. defer 能修改返回值吗?
如果修改的是命名返回值,可以影响最终结果:
go
func getNumber() (result int) {
defer func() {
result++
}()
return 10
}
最终返回:
text
11
如果修改的只是普通局部变量,通常不会改变已经计算并准备好的返回结果:
go
func getNumber() int {
result := 10
defer func() {
result++
}()
return result
}
最终返回:
text
10
这一阶段的知识点总结
这一阶段的实际学习范围可以整理成下面这张表:
表格
| 内容 | 示例或规则 |
|---|---|
| 命名返回值 | func getValue() (result int) |
| 命名返回值零值 | int 为 0,string 为 "",bool 为 false |
| 裸返回 | return |
| 显式返回 | return result |
| 登记延迟调用 | defer cleanup() |
defer 执行时机 |
当前函数真正退出前 |
| 提前返回 | 已登记的 defer 仍会执行 |
多个 defer |
后进先出 |
| 参数求值时机 | 执行到 defer 语句时 |
| 匿名函数体求值 | 匿名函数真正执行时 |
| 修改命名返回值 | 可以影响最终结果 |
| 修改普通局部变量 | 不影响已经准备好的返回结果 |
最重要的几条规则是:
text
defer 不是异步任务
text
调用延迟执行,调用参数立即求值
text
多个 defer 后进先出
text
return 设置返回值后,还要执行 defer
text
defer 可以修改命名返回值
写在最后
这一阶段,我从命名返回值开始,一路学到了:
text
命名返回值
→ 返回值零值
→ 裸 return
→ defer
→ 提前 return 时执行 defer
→ 多个 defer 后进先出
→ defer 参数的求值时机
→ defer 修改命名返回值
defer 给我最深的印象是:
代码出现的位置,不一定等于代码真正执行的时间。
它写在函数中间,却可能在最后执行。
函数遇到 return 后,它仍然会运行。
它甚至可以在函数准备返回后,继续修改命名返回值。
这些能力确实很灵活,但灵活也意味着需要更加谨慎。
特别是命名返回值、裸返回和 defer 放在一起时,如果使用得太复杂,很容易让一个函数的最终结果变得不直观。
所以目前我的结论是:
text
语法需要学会
→ 执行顺序需要理解
→ 阅读代码时需要注意
→ 实际使用要优先考虑可读性
对于前端开发者来说,可以暂时这样建立对应关系:
text
Go defer
→ 更接近 JavaScript 的 finally
→ 不是 setTimeout
→ 也不是异步任务
但这个类比只能帮助理解"退出前执行收尾操作"。
真正使用时,还是要记住 Go 自己的规则:
text
多个 defer 后进先出
参数在登记时求值
匿名函数体在真正执行时求值
defer 可以修改命名返回值
下一阶段,我会继续学习 Go 中的匿名函数和闭包。
这部分听起来应该会比 defer 熟悉一些。
毕竟 JavaScript 开发每天都在和函数表达式、箭头函数以及闭包打交道。
但 Go 的闭包和 JavaScript 闭包是否完全一样,还需要继续通过代码验证。
还是按照之前的方式:
text
先写代码
→ 主动修改变量
→ 观察输出结果
→ 分析变量到底从哪里读取
→ 再整理成文章
下一篇见。