网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- 命名返回(指定返回值命名func test() (t int)),执行 return 语句时,并不会再创建临时变量保存,defer修改的是返回值。
func test() (i int) {
defer func() {
i++
fmt.Println("defer1:", i)
}()
defer func() {
i++
fmt.Println("defer2:", i)
}()
return i
}
func main() {
fmt.Println("return:", test())
}
解释:
这里已经指明了返回值就是 i ,所以后续对 i 进行修改都相当于在修改返回值,所以最终返回2。
- 函数返回值为指针,指向变量所在的地址,defer修改变量,指针指向的地址不变,地址对应的内容发生了改变,返回值改变。
func test() \*int {
var i int
defer func() {
i++
fmt.Println("defer1:", i)
}()
defer func() {
i++
fmt.Println("defer2:", i)
}()
return &i
}
func main() {
fmt.Println("return:", \*test())
}
解释:
此时的返回值是一个指针(地址),这个指针 = &i,相当于指向变量 i 所在的地址,两个defer语句都对 i 进行了修改,那么返回值指向的地址的内容也发生了改变,所以最终返回2。
- 特殊例子:defer没有修改有名返回值,因为 r 作为参数,传入defer 内部时会发生值拷贝,地址会变,defer修改的是新地址的变量,不是原来的返回值。
func test() (r int) {
defer func(r int) {
r++
fmt.Println("defer:", r)
}(r)
return r
}
func main() {
fmt.Println("return:", test())
}
解释:
最初返回值 r = 0, r 作为参数传入defer 内部会发生值拷贝,相对于一个新的变量 r’,defer 内部内的语句相当于 r’ = r’ +5,r 并没有被修改,所以最终返回 0 。
4、defer 与 panic 的执行逻辑
panic 其实是一个终止函数栈执行的过程,类似其它语言中的抛出异常,但是在函数退出前都会执行defer里面的函数,直到所有的函数都退出后,才会执行panic。
在panic语句后面的defer语句不被执行,在panic语句前的defer语句会被执行(早于panic),panic触发defer出栈。可以在defer中使用recover捕获异常,panic 后依然有效。
func main() {
defer fmt.Println("defer before panic")
panic("panic")
defer fmt.Println("defer after panic")
}
5、defer 与 recover
recover 是 go 提供的一个用来截获 panic 信息,重新获取协程控制的函数。
使用要求:
- recover 必须在 defer 函数中使用,但是不能被 defer 直接调用;
//以下捕获失败
defer recover()
defer fmt.Prinntln(recover)
defer func(){
func(){
recover() //无效,嵌套两层
}()
}()
//以下捕获有效
defer func(){
recover()
}()
func except(){
recover()
}
func test(){
defer except()
panic("runtime error")
}
- 多个 panic 仅有最后一个可以被 recover捕获。后面的 panic 会覆盖掉之前的。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("panic three")
}()
defer func() {
panic("panic two")
}()
panic("panic one")
}
- recover 只能恢复同一个协程中的 panic ,不能跨协程捕获panic 信息,所以 recover 必须与可能发生panic的协程在同一个协程中才生效。panic 在子协程中,而 recover 在主协程中,recover 捕获不到 panic,最终会导致所有的协程全部挂掉,程序会整体退出。
错误示例:
func Test() {
panic("发生异常了!!!")
}
func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
go Test()
time.Sleep(5 \* time.Second)
fmt.Println("main()不能正常执行!!!")
}
正确示例:
func Test() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic("发生异常了!!!")
}
func main() {
go Test()
time.Sleep(5 \* time.Second)
fmt.Println("main()能正常执行!!!")
}
三、defer 其他用处
1、关闭文件
func main() {
// 只读方式打开指定目录下的文件 os.Open()
file, err := os.Open("C:\\Users\\lenovo\\Desktop\\test.txt")
if err != nil {
fmt.Println("open file failed!!! err: ", err)
return
}
// 关闭文件 close()
defer file.Close()
}
2、释放互斥锁
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
func main() {
lookup("test")
}
四、defer 使用中的一些坑
坑1:defer在匿名返回值和命名返回值函数中的不同表现
- 匿名返回,执行 return 语句后,Go会创建一个临时变量保存返回值,defer修改的是临时变量,没有修改返回值。
- 命名返回,执行 return 语句时,并不会再创建临时变量保存,defer修改的是返回值。
坑2:在for循环中使用defer可能导致的性能问题
func deferInLoops() {
for i := 0; i < 100; i++ {
f, \_ := os.Open("/etc/hosts")
defer f.Close()
}
}
defer在紧邻创建资源的语句后执行,看上去逻辑没有什么问题,但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。
所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。
坑3:判断执行没有err之后,再defer释放资源
一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。
正确写法:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-ulgsgzXB-1715401976251)]
[外链图片转存中…(img-d86WZ7mh-1715401976252)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!