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。
小结
- defer可以修改有名返回值,要注意不要在defer中申请同名变量哦,不然无法修改成功
- defer的参数是 copy 且 立马获得的,不会等到要执行的时候再去获取参数
- defer中 非参数非局部的变量 会在外获取变量本身