Go-关键字defer、panic、recover详解

简介: Go-关键字defer、panic、recover详解

本文进行了关键字defer和内建函数panic、recover的介绍和使用细节。

defer

Go 语言的 defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。

在文章Go-函数详解(参数、返回值、init函数、匿名函数、defer)中进行了简单的使用,但是还不够深入,于是在知乎提了问题,今天做下总结。

调用时机

代码

//------调用时机:所在函数结束或返回前------
func callTime()  {
  {
    defer fmt.Println("defer in callTime()")
    fmt.Println("code block finish..")
  }
  fmt.Println("callTime() finish...")
}

结果

code block finish..

callTime() finish...

defer in callTime()

调用时机在函数/方法结束或返回前

多次调用时的执行顺序

代码

func moreDefer()  {
  for i:=1;i<5;i++{
    defer fmt.Println("defer",i)
  }
  fmt.Println("moreDefer() finish...")
}

结果

moreDefer() finish...

defer 4

defer 3

defer 2

defer 1

栈的顺序调用,先入后出。

传参问题

代码

func deferPara()  {
  i := 0
  defer fmt.Println("defer",i,"in deferPara()")
  i++
  fmt.Println("deferPara finish...,i is ", i)
}

或许你认为结果是这样的:

deferPara finish...,i is  1

defer 1 in deferPara()

因为defer在函数结束前运行嘛,但事实上结果是这样的:

deferPara finish...,i is  1
defer 0 in deferPara()

defer会在到达所在行时,就将变量复制一份传过去。想到的解决方案如下:

  • 引用类型就没有问题了
  • 如果参数是值类型,你的defer不修改参数,你可以传地址。
  • 如果参数是值类型,你也可以将defer放在函数/方法不修改参数后。
  • 如果参数是值类型,你可以使用匿名函数,函数体内再用参数。
func paraFix()  {
  i := 0
  defer fmt.Println("send addr:defer",&i,"in paraFix()")
  defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
  i++
  defer fmt.Println("put defer later:defer",i,"in deferPara()")
}

源代码

结构体

src->runtime->runtime2.go

type _defer struct {
  siz     int32 // 包含参数和结果
  started bool  // 是否开始
  heap    bool  // 是否分配在堆上
  openDefer bool // 是否开放编码
  sp        uintptr  // 栈指针
  pc        uintptr  // 程序计数器
  fn        *funcval // 开放编码时可为nil
  _panic    *_panic  // 正在运行的defer的panic
  link      *_defer  // _defer指针
  fd   unsafe.Pointer // 预分配的函数数据
  varp uintptr        
  framepc uintptr
}

_defer是一个单链表(链栈),采用头插的方式,取的时候先取头的。

编译

src->cmd->compoile->internal->gc->ssa.go stmt方法的一个case

case ODEFER:
    if Debug_defer > 0 {
      var defertype string
      if s.hasOpenDefers {
        defertype = "open-coded"
      } else if n.Esc == EscNever {
        defertype = "stack-allocated"
      } else {
        defertype = "heap-allocated"
      }
      Warnl(n.Pos, "%s defer", defertype)
    }
    if s.hasOpenDefers {
      s.openDeferRecord(n.Left)
    } else {
      d := callDefer
      if n.Esc == EscNever {
        d = callDeferStack
      }
      s.callResult(n.Left, d)
    }

有些defer将在栈上分配,有些在堆上分配。首先是开放编码进行优化,其次是栈,最后是堆,分配到栈上可以节约内存分配带来的额外开销。

panic

执行defer

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer机制)。随后,程序崩溃并输出日志信息。

func panicDefer()  {
  fmt.Println("code before panic")
  defer fmt.Println("defer in panicDefer")
  panic("something wrong in panic Defer")
  fmt.Println("code after panic")
}

结果

code before panic

defer in panicDefer

panic: something wrong in panic Defer


goroutine 1 [running]:

main.panicDefer()

       E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:42 +0x10a

main.main()

       E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:62 +0x27

exit status 2

利用defer就可以实现有panic时也能进行资源释放等。

协程问题

panic 只会触发当前 goroutine 的 defer

func panicGoroutine()  {
  defer println("defer in main")
  go func() {
    defer println("defer in goroutine")
    panic("something wrong...")
  }()
  time.Sleep(time.Second)
}

结果:

defer in goroutine

panic: something wrong...


goroutine 6 [running]:

main.panicGoroutine.func1()

       E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:50 +0x78

created by main.panicGoroutine

       E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:48 +0x78

exit status 2

协程外面的defer执行不了

源代码

结构体

src->runtime->runtime2.go

type _panic struct {
  argp      unsafe.Pointer // 指向defer栈的函数指针
  arg       interface{}    // panic参数
  link      *_panic        // 先前panic的指针
  pc        uintptr        // 运行时,此panic被绕过时返回到哪
  sp        unsafe.Pointer // 运行时,此panic被绕过时返回到哪
  recovered bool           // 是否此panic结束
  aborted   bool           // 这个panic被终止
  goexit    bool
}

崩溃

src->runtime->panic.go

可以查看gopanic、fatalpanic两个函数,代码过多,不黏贴了,有兴趣可以看看。下面是fatalpanic的部分代码:
  systemstack(func() {
    exit(2)
  })

这就能看出前面panic时为什么是“exit status 2”了

recover

使用

recover 可以中止 panic 造成的程序崩溃,只能在 defer 中发挥作用

代码

func recoverDefer()  {
  //defer println("defer in main") // 执行不到
  defer func() {
    if err := recover();err!=nil{
      println("defer in main")
      println(err)
    }
  }()
  go func() {
    defer println("defer in goroutine")
    panic("something wrong...")
  }()
  panic("something wrong in recoverDefer()")
  time.Sleep(time.Second)
}

结果

defer in main

(0xf4d940,0xf85bc8)

defer in goroutine

源代码

src->runtime->panic.go

func gorecover(argp uintptr) interface{} {
  gp := getg()
  p := gp._panic
  if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
    p.recovered = true
    return p.arg
  }
  return nil
}

可以看到,p!=nil的话才行,也就是说你在panic前使用recover,没在defer中,那么返回的是nil,也就是recover失效了。如果不是nil,在gopanic中会进行处理。

总结

defer

  • 调用时机:函数或方法在返回或结束前执行
  • 多次调用:先写的后调用,栈的顺序
  • 传参问题:值类型时,参数不改变后使用defer、defer的函数不修改则传地址、匿名函数函数体中使用

优点:

  • panic后执行defer,防止异常时忘记释放资源
  • 函数复杂分支返回,写一次即可,简洁,复用性好

panic

  • panic后执行本协程的defer
  • 跨协程问题使用recover解决

recover

  • 终止panic造成的崩溃
  • defer中使用时才有效

全部源代码

1.package main
import (
  "fmt"
  "time"
)
//------调用时机:所在函数结束或返回前------
func callTime()  {
  {
    defer fmt.Println("defer in callTime()")
    fmt.Println("code block finish..")
  }
  fmt.Println("callTime() finish...")
}
//-----多个defer的顺序------
func moreDefer()  {
  for i:=1;i<5;i++{
    defer fmt.Println("defer",i)
  }
  fmt.Println("moreDefer() finish...")
}
//-----defer传参问题-----
func deferPara()  {
  i := 0
  defer fmt.Println("defer",i,"in deferPara()")
  i++
  fmt.Println("deferPara finish...,i is ", i)
}
//-----defer传参修复方案--------
func paraFix()  {
  i := 0
  defer fmt.Println("send addr:defer",&i,"in paraFix()")
  defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
  i++
  defer fmt.Println("put defer later:defer",i,"in deferPara()")
}
//----panic后执行defer-------
func panicDefer()  {
  fmt.Println("code before panic")
  defer fmt.Println("defer in panicDefer")
  panic("something wrong in panic Defer")
  fmt.Println("code after panic")
}
//----跨协程 defer panic问题-----
func panicGoroutine()  {
  defer println("defer in main")
  go func() {
    defer println("defer in goroutine")
    panic("something wrong...")
  }()
  time.Sleep(time.Second)
}
//----------defer中使用recover---------
func recoverDefer()  {
  //defer println("defer in main") // 执行不到
  defer func() {
    if err := recover();err!=nil{
      println("defer in main")
      println(err)
    }
  }()
  go func() {
    defer println("defer in goroutine")
    panic("something wrong...")
  }()
  panic("something wrong in recoverDefer()")
  time.Sleep(time.Second)
}
func main() {
  //callTime()
  //moreDefer()
  //deferPara()
  //paraFix()
  //panicDefer()
  //panicGoroutine()
  recoverDefer()
}

参考

知乎-在go语言中,为什么使用defer?

go-1.16.3源代码

更多Go相关内容:Go-Golang学习总结笔记

有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系

相关文章
|
12月前
|
安全 Go
defer关键字:延迟调用机制-《Go语言实战指南》
Go 语言中的 `defer` 是用于延迟执行函数调用的关键字,广泛应用于资源释放、异常捕获和日志记录等场景。它在函数返回前执行,支持栈式后进先出(LIFO)顺序,参数求值时机为声明时而非执行时。常见用法包括文件关闭、锁解锁及结合 `recover` 处理 panic。尽管高效,频繁使用可能带来性能开销,需谨慎处理。总结而言,`defer` 是构建健壮代码的核心工具之一。
|
Go 索引
go语言使用range关键字
go语言使用range关键字
214 7
|
安全 Go 索引
go语言使用 range 关键字
go语言使用 range 关键字
245 3
|
测试技术 Go 索引
go语言使用 range 关键字遍历
go语言使用 range 关键字遍历
272 3
掌握 Go 语言的 defer 关键字
掌握 Go 语言的 defer 关键字
|
安全 Go
Go语言的iota关键字有什么用途?
**Go语言中的`iota`是常量生成器,用于在`const`声明中创建递增的常量。`iota`在每个新的`const`块重置为0,然后逐行递增,简化了枚举类型或常量序列的定义。例如,定义星期枚举:** ```markdown ```go type Weekday int const ( Sunday Weekday = iota // 0 Monday // 1 Tuesday // 2 ... ) ``` 同样,`iota`可用于定义不同组的常量,如状态码和标志位,保持各自组内的递增,提高代码可读性。
256 1
实验深度理解Go中try...catch...的panic、defer、recover用法
文章通过实验代码演示了Go语言中如何使用panic、defer和recover函数来模拟try...catch...的异常处理机制,并详细解释了每个函数的作用和在异常处理中的使用场景。
310 0
|
安全 Go
Go语言的iota关键字有什么用途?
在Go语言中, `iota` 是一个特殊常量生成器, 用于在 `const` 声明中创建递增的常量值。每当遇到新的 `const` 关键字时, `iota` 会重置为0并在每个常量声明行上递增1。这非常适合定义枚举类型或一组相关常量。`iota` 大大简化了枚举类型的定义过程, 并提供了类型安全的方法来表示固定值集合, 对于错误码、状态码等非常有用。
324 0
go语言panic异常及recover拦截恢复初级
go语言panic异常及recover拦截恢复初级 Go语言为我们提供了专用于“拦截”运行时panic的内建函数——recover。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
2789 0
|
7月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
373 2

热门文章

最新文章