Go语言基本认知 | defer的坑与应用 | 探讨defer

531 阅读3分钟

defer的坑与应用【第一节】

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一、使用defer修改返回值

实际应用中可以使用defer修改有名返回值,具体操作如下:

func GetNum() (x int) {
	x = 10
	defer func() {
		x = 20
	}()
	return
}

但是需要注意,不要在defer中申请同名参数,如果使用以下形式,那么有由于局部参数的问题,会导致修改失败

func hello() (x int) {
	x = 10
	defer func() {
        x := 20 // 注意改成了 := 而非 =
		fmt.Println(x) // 虽然这里打印20,但是其实这个x是临时申请出来的,所以返回值仍然是10
	}()
	return
}

二、defer函数的参数

defer函数的参数是会 Copy 一份的,如果是调用了某个方法获取的参数,那么这个方法不会等待defer这个函数弹出的时候再去获取,而是一开始获取该参数

举个例子:

func main() {
	startedAt := time.Now()
	defer fmt.Println(time.Since(startedAt))
	time.Sleep(time.Second)
}

实际输出的是:0s。这是因为我们 defer 调用的方法是Println,它的参数( time.Since(startedAt) )在第一时间就获取了,而不是函数结束再执行defer的时候获取参数。

给个更具体的例子感受一下:

func getName() string {
	fmt.Println("调用了 getName()")
	return "num"
}

func main() {
	defer fmt.Println(getName())
	fmt.Println("Over")
}

输出的结果是:

调用了 getName()
Over
num

从这里可以看出来参数获取是直接执行的,“调用了 getName()”这句话在“Over”之前

所以如果我们要后面再使用,我们需要将其改变为非参的情况:

func getName() string {
	fmt.Println("调用了 getName()")
	return "num"
}

func main() {
	defer func() {
		fmt.Println(getName())
	}()
	fmt.Println("Over")
}

这样就可以保证打印顺序了:

func getName() string {
	fmt.Println("调用了 getName()")
	return "num"
}

func main() {
	defer func() {
		fmt.Println(getName())
	}()
	fmt.Println("Over")
}

同理之前提到的获取时间的例子改为:

func main() {
	startedAt := time.Now()
	defer func() {
		fmt.Println(time.Since(startedAt))
	}()
	time.Sleep(time.Second)
}

接下来我们考虑参数是否为copy的情况

有以下这两个程序对比(仅仅是为了展示而设计的程序,实际应用中不建议for中出现defer):

// 程序1
func main() {
	for i := 0; i < 3; i++ {
		defer fmt.Println(i)
	}
}
// 程序2
func main() {
	for i := 0; i < 3; i++ {
		defer func() {
			fmt.Println(i)
		}()
	}
}

两个程序显示有区别吗?实际显示如下:

// 程序1
2
1
0
// 程序2
3
3
3

这是为什么呢?

其实是因为程序1我们传入的 i 是作为参数的,参数是copy的,所以后面 i 改变了不影响,但是程序2的 i 不是作为参数,而是取原本 i 的地址,会获取到原本的 i ,最后原本的 i 已经编成 3 了,自然结果全为 3。

小结

  1. defer可以修改有名返回值,要注意不要在defer中申请同名变量哦,不然无法修改成功
  2. defer的参数是 copy 且 立马获得的,不会等到要执行的时候再去获取参数
  3. defer中 非参数非局部的变量 会在外获取变量本身