这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。
1. 返回值的处理:根据 Go 关于 [Return Value](
https://go.dev/ref/spec#Return_statements) 的规范,当你声明一个返回值的时候,你实际上是声明了一个临时对象,区别仅存在于这个返回对象是有名字还是没有名字的;
2. Defer 的作用时机:根据 Go 关于 [Defer Statement](
https://go.dev/ref/spec#Defer_statements) 的规范,`defer` 的作用时机在 `return` 的所有 Value 都被计算且赋值完毕后,真实返回前执行;
3. Go 对闭包的处理:根据 Go 关于 [Function literals](
https://go.dev/ref/spec#Function_literals) 的规范,闭包内捕获的自由变量会被共享;换句话说,你可以理解为闭包实际上捕获了外部变量的指针,对其的修改会同步到原始对象。
花点时间理解上面三个规范会带来的代码作用。
现在我们来分析代码:
- 在 Case1 中,由于返回值被具名了,`return t` 实际上可以理解为 `t = t; return`,也就是仅仅重新赋值,之后执行的 `defer` 重新修改了 `t`,导致返回的值产生了变动。
- 在 Case2 中,由于返回值匿名,假定返回值是一个隐藏变量 `tForReturn`,`return t` 实际上可以理解为 `tForReturn = t; return`,此时虽然你的 `defer` 修改了 `t`,但是由于返回的对象是 `tForReturn`,获取的返回值并没有发生变化,一切正常。当然,此时你再次在 `foo` 调用后查看 `t` 的值,它确实也会是 `4`,`defer` 的作用生效了。
P.S. Go 的规范对这种行为没有明确的规定,上面的三个 spec 其实也只能说是“模糊”描述了作用原理,还是要观察编译器的实现实锤,这也是这门语言天天开天窗擦屁股的核心包袱之一;具名返回值这个特性本身也有很强的“拍脑袋”属性,它确实有用,但是没有有用到这个程度,结果反而引入了更多的混淆。