defer语句用于延迟函数的调用,每次 defer 都会把一个函数压入栈中,函数返回前(return 执行后)再把延迟的函数取出并执行。

测试题

先做几道测试题熟悉一下

测试 1: 参数在 defer 语句出现时已经确定下来

// https://play.golang.org/p/ydxH3vUiE76
func main() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}
  • 答案: 输出 0
  • 解答: 在程序运行到 defer 那一行语句时已经确定了 i 的值,后面无论怎么修改值都不会变

测试 2: 参数在 defer 语句出现时已经确定下来

// https://play.golang.org/p/iS-sKerhEOv
func main() {
	a := [3]int{1, 2, 3}

	defer fmt.Println(&a)

	a[0] = 3
	return
}
  • 答案: &[3,2,3]
  • 解决: 在执行 defer 时 fmt 的参数已经确定,即 a 的地址,所以下面修改 a 也会体现到 defer 的 fmt 上

测试 3: 匿名函数 和 defer

  • 问题: 代码输出结果
// https://play.golang.org/p/NkJ2t5xu02K
func main() {
	i := 0

	defer func() {
		fmt.Println("i:", i)
	}()

	i++
	return
}
  • 答案: 1
  • 解决: 这个和 “测试 1” 很像,只是这个 defer 是个匿名函数,此时 defer 中的 i 可以理解为外部 i 的引用,所以此时 i++ 可以生效

测试 4: defer 更新有名返回值

// https://play.golang.org/p/D5xkNSwW9Hr
func a() (result int) {
	i := 1

	defer func() {
		result++
	}()

	return i
}
  • 答案: 2
  • 解答: return 语句先把 result 设为 1,再执行 defer 的 result 加 1,最终返回 2,defer 可以更新返回值

测试 5: return 和 defer 的执行顺序

// https://play.golang.org/p/Zr4y8Bo0YSd
func a() (result int) {
	i := 1

	defer func() {
		i++
	}()

	return i
}
  • 答案: 1
  • 解答: return 语句先把 result 设为 1,再执行 defer 的 i++,所以最后返回只是 result 的值,为 1,defer 是在 return 执行完后执行的。

测试 6: 匿名返回值 和 defer

// https://play.golang.org/p/AiQx5z0GJa4
func a() int {
	i := 1

	defer func() {
		i++
	}()

	return i
}
  • 答案: 输出 1
  • 解答: 这涉及有名返回值和匿名返回值的区别。return 会把返回值先存储起来,对与匿名返回值来说是存储在临时变量里,而 defer 函数无法获取到这个临时变量,所以返回临时变量的值,(ps: 还涉及到了匿名函数的变量引用,匿名函数理解为 i 的值是引用类型)

测试 7: 有名返回值 和 defer

// https://play.golang.org/p/2LBBrF2WZpC
func a() (i int) {
	i = 1

	defer func() {
		i++
	}()

	return i
}
  • 答案: 输出 2
  • 解答: 对于有名返回值,ruturn 会把返回值保存到已命名的变量中,defer 获得此变量并进行操作

defer 规则

延迟函数的参数在 defer 语句出现时就已经确定下来了

// https://play.golang.org/p/FN_b_SMO3gJ
func a() {
	i := 0

	defer fmt.Println(i)

	i++
	return
}

defer 语句中的 i 在 defer 出现时已经确定了,实际上是拷贝了一份,后面对变量 i 的修改不会影响 defer 中 fmt 的执行

对于指针类型的参数,规则仍然使用,因为拷贝的是地址,所以 defer 后对变量的修改会影响到 defer 语句执行的结果

defer 函数在当前函数返回之后以后进先出的顺序执行

// https://play.golang.org/p/xN6dBnoFR-k
func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Println(i)
	}
}

定义 defer 类似于入栈操作,执行 defer 类似出栈操作。后进先出

defer 函数可能会读取修改有名返回值

return 不是一个原子操作

对于语句 return i,实际上分了两步,即将 i 值存入栈中作为返回值,然后执行跳转。而 defer 的执行时机正是跳转前,所以 defer 执行时是有机会操作返回值的

// https://play.golang.org/p/OWW2rhFAFpu
func a() (result int) {
	i := 1

	defer func() {}
		result++
	}()

	return i
}

该函数的 return 语句可以拆分为下面两行

result = i
return // 只是跳转

加入 defer 之后则是

result = i
result++
return

只要把 return 语句执行拆开就可以很好理解上面的测试了

主函数拥有匿名返回值,返回字面值

字面值即 “1”、“2”、“hello” 这样的值,主函数拥有匿名返回值,返回时使用字面值,defer 无法操作返回值

// https://play.golang.org/p/0Q5lj-GMd6c
func a() int {
	var i int

	defer func() {
		i++
	}()

	return 99
}

return 语句直接把 99 写入到栈中作为返回值,defer 无法操作该返回值,所以不能影响返回值

主函数拥有匿名返回值,返回变量

// https://play.golang.org/p/IXtt57EuchT
func a() int {
	var i int

	defer func() {
		i++
	}()

	return i
}

主函数拥有匿名返回值,返回使用本地或全局变量,defer 可以引用到返回值,但不会改变返回值,return 会把 i 赋值给一个临时变量。以上面的例子为例,假设临时变量为 tmp

tmp = i
i++
return

虽然 defer 修改了 i 的值,但是 return 时返回的是 tmp,所以不会对函数的返回值产生影响

主函数拥有有名返回值

主函数语句中带名字的返回值,会被初始化成一个局部变量,函数内部像使用局部变量一样使用该返回值,defer 语句可能会改变返回结果

// https://play.golang.org/p/RNOmjn9lITx
func a() (ret int) {
	defer func() {
		ret++
	}()

	return 0
}

拆解 return 操作

ret = 0
ret++
return

在函数返回前,在 defer 中对返回值做了 ret++ 操作

参考链接