如何在Go语言中检测panic(nil)和延迟函数的正常执行?

huangapple go评论165阅读模式
英文:

How to detect panic(nil) and normal execution in deferred function Go?

问题

Go运行时可以检测到panic(nil)并报告错误。

然而,我无法通过在defer函数中使用recover()来检测到panic(nil),因为它返回nil,所以我无法将其与正常执行(无panic)区分开来,就像我会测试recover()的返回值是否为nil一样。

例如,

defer func(){
    var err = recover()
    if err != nil {
       // 真正严重的情况。内部代码引发了panic。
       // 并且我们可能有一些必须在任何情况下清理的关键资源。
       // 但是对于panic(nil),这段代码将不会被执行。

       回滚()

       // 我仍然不确定如何处理`panic`...
       // 我应该忽略它们吗?
    }
}()

var err = doTransaction()
if err == nil {
    提交()    // 正常情况。
} else {
    回滚()  // 常规执行。只是幸运的情况。
}

ROLLBACK只是一个示例,我认为我可能有很多需要清理的关键情况。嗯,这些清理代码在真正的程序崩溃时也不会被执行,但我希望尽可能地防御。

如何在延迟函数中检测到任何panic,而不考虑其参数?

英文:

The go runtime can detect panic(nil) and reports an error.

However, I can't detect panic(nil) with recover() in a deferred function because it returns nil, so I cannot differentiate it from normal execution (no panic) as I would test for
the return value of recover() to be nil.

For example,

defer func(){
    var err = recover()
    if err != nil {
       // Real serious situation. Panic from inner code.
       // And we may have some critical resources which 
       // must be cleaned-up at any cases.
       // However, this will not be executed for panic(nil) 

       rollback()

       // I am still not sure that how should I treat `panic`…
       // Should I just ignore them?
    }
}()

var err = doTransaction()
if err == nil {
    commit()    // Happy case.
} else {
    rollback()  // Regular execution. Just a lucky case.
}

ROLLBACK is just an example, and I think I can have plenty of critical cases needs cleanup. Well, those cleanup code won't be executed on real program crash too, but I want to defend as much as possible.

How can I detect any panic regardless of its parameter in a deferred function?

答案1

得分: 5

我只会翻译你提供的内容,以下是翻译结果:

我只是在退出之前设置一个标志。

据我所知,panic是与goroutine相关的,并且单个goroutine保证在单个线程中。因此,在变量ok周围不需要同步/锁定。如果我错了,请纠正我。

func clean(ok *bool) {
    if *ok {
        log.Printf("执行正常。未检测到panic。\n")
    } else {
        var reason = recover()
        log.Printf("发生了一些糟糕的事情。原因 = %v\n", reason)
        panic("异常退出。程序被放弃。堆栈跟踪在此处。")
        debug.PrintStack() // 哎呀,这个不会运行。
    }
}

func main() {
    var ok bool = false

    defer clean(&ok)
    panic(nil)

    test1() // 这是主要的工作。

    ok = true
    log.Printf("所有工作已完成。优雅地退出。\n")
}

希望对你有帮助!

英文:

I simply can set a flag before exit.

AFAIK, panic is goroutine-specific, and single goroutine is guaranteed to be in single thread. So synchronization/locking is not required around variable ok. If I'm wrong, please correct me.

func clean(ok *bool) {
	if *ok {
		log.Printf("Execution OK. No panic detected.\n")
	} else {
		var reason = recover()
		log.Printf("Some bad thing happen. reason = %v\n", reason)
		panic("Abnormal exit. Program abandoned. Stack-trace here.")
		debug.PrintStack() // Oops. this will not run.
	}
}

func main() {
	var ok bool = false

	defer clean(&ok)
	panic(nil)

	test1() // Here's the main job.

	ok = true
	log.Printf("All work done. Quit gracefully.\n")
}

答案2

得分: 4

除非我误解了你的问题,否则即使传递的值为nil,延迟函数调用在发生恐慌时也会运行。这可以通过以下程序进行说明:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("Recover:", recover())
    }()
    panic(nil)
}

因此,你可以通过将recover()返回的值与nil进行比较,轻松检测到是否发生了panic(nil)

编辑以回答评论:

是的,没错,延迟调用通常在函数返回时运行。但它们也会在panic()后展开调用堆栈时运行。

在问题更新后进行编辑:

你说得对,没有办法区分这些情况。另一方面,使用nil引发恐慌也没有太多意义,特别是因为这个限制。

我能想到的唯一使用panic(nil)的用例是有意避免恢复并强制程序崩溃并输出堆栈跟踪。不过,有更优雅的方法来实现这一点,例如使用runtime包。

英文:

Unless I misunderstood your question, deferred function calls will run when panicking, even if the value passed was nil. This is illustrated by the following program:

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("Recover:", recover())
	}()
	panic(nil)
}

You can therefore easily detect if panic(nil) happened by comparing the value returned by recover() to nil.

Edit to answer comment:

Yes, that's true; deferred calls will normally run when a function returns. But they are also run when unwinding the call-stack after a panic().

Edit after question was updated:

You are correct that there is no way to differentiate these cases. On the other hand, panicking with nil doesn't make much sense either - especially because of this limitation.

The only use-case for panic(nil) that I could think of would be to deliberately avoid recovery and force the program to crash with a stack trace. There are more elegant ways to do that though, using the runtime package for instance.

答案3

得分: 2

检查一下提案 25448“spec: 保证从 recover 返回非 nil 值”是否有帮助。

在 Go 1 中,允许使用 nil 的 panic 值进行调用,但这很奇怪。

几乎所有的代码都会检查 panic,例如:

defer func() {
    if e := recover(); e != nil { ... }
}()

...但是在 panic(nil) 的情况下,这种方式是不正确的。

正确的方式更像是:

panicked := true
defer func() {
    if panicked {
        e := recover()
        ...
    }
}()
...
panicked = false
return
....
panicked = false
return

提案是:使运行时的 panic 函数将其 panic 值从 nil 提升为类似于 runtime.NilPanic 的全局值,该值是私有的、不可赋值的类型:

package runtime

type nilPanic struct{}
 
// NilPanic 是当代码以 nil 值 panic 时,由 recover 返回的值。
var NilPanic nilPanic

该提案于 2018 年提出,刚刚在 2023 年 1 月被接受。

当前的提案是,在 Go 1.21(假设)开始,panic(nil)panic 期间会变成 panic(&runtime.PanicNil{})(因此,一个恰好是 nil 接口的变量也会这样,而不仅仅是字面文本 panic(nil))。

在这个改变之后,panic(nil) 仍然是可以的,但是 recover() 返回的是 &runtime.PanicNil{} 而不是 nil。如果设置了 GODEBUG=panicnil=1,则禁用此更改,panic(nil) 会导致 recover() 像以前一样返回 nil

假设 #56986(“proposal: extended backwards compatibility for Go”)也在 Go 1.21 中实现,那么这种行为只会在模块中发生变化,这些模块在工作模块(顶级 go.mod)中声明了 go 1.21。因此,当你从 Go 1.20 更新到 Go 1.21 时,而不更改任何 go.mod 行,你仍然会得到旧的 panic(nil) 行为。当你将顶级 go.mod 更改为 go 1.21 时,整个程序都会采用新的行为。

英文:

Check if the proposal 25448 "spec: guarantee non-nil return value from recover" can help.
>
> Calling panic with a nil panic value is allowed in Go 1, but weird.
>
> Almost all code checks for panics with:
>
> go
> defer func() {
> if e := recover(); e != nil { ... }
> }()
>

> ... which is not correct in the case of panic(nil).
>
> The proper way is more like:
>
> go
> panicked := true
> defer func() {
> if panicked {
> e := recover()
> ...
> }
> }()
> ...
> panicked = false
> return
> ....
> panicked = false
> return
>

>
> Proposal: make the runtime panic function promote its panic value from nil to something like a runtime.NilPanic global value of private, unassignable type:
>
> go
> package runtime
>
> type nilPanic struct{}
>
> // NilPanic is the value returned by recover when code panics with a nil value.
> var NilPanic nilPanic
>


Proposed in 2018, it just got accepted (Jan. 2023)

> The current proposal is that starting in Go 1.21 (say),
panic(nil) turns into panic(&runtime.PanicNil{}) during panic (so a variable that happens to be a nil interface does it too, not just the literal text panic(nil)).
>
>panic(nil) is always OK, but recover() returns &runtime.PanicNil{} instead of nil after this change.
If GODEBUG=panicnil=1, then this change is disabled and panic(nil) causes recover() to return nil as it always has.
>
> Assuming #56986 ("proposal: extended backwards compatibility for Go") happens too in Go 1.21, this behavior would only change in modules that say 'go 1.21' in the work module (top-level go.mod).
So when you update from Go 1.20 to Go 1.21, without changing any go.mod lines, you'd still get the old panic(nil) behavior.
When you change your top-level go.mod to say go 1.21, then you get the new behavior, in the whole program.

答案4

得分: 1

对于大多数情况来说,if recover() != nil 的惯用方式是有效的,但为了保证健壮性,或者在执行第三方代码时,不应该使用该方式,因为它无法检测到 panic(nil)

以下是检查的模式:

func checkPanic(action func()) (panicked bool, panicMsg interface{}) {
    panicked = true
    defer func() { panicMsg = recover() }()
    action()
    panicked = false
    return
}

如果 action() 发生 panic,那么下面的代码行将不会被执行,panicked 保持为 true。
你可以使用上述辅助函数进行检查,示例如下:

if panicked, msg := checkPanic(func() { /* ... */ }); panicked {
    // 处理 panic...
} else {
    // 正常情况 :D
}

或者你可以直接将该模式实现到你想要检查的函数中,但这样可能会变得更加混乱。

英文:

For most cases, the idiomatic way of if recover() != nil works, but for robustness, or when executing third-party code, it should not be used, because it does not detect panic(nil).

This pattern is how you can check it:

func checkPanic(action func()) (panicked bool, panicMsg interface{}) {
    panicked = true
    defer func() { panicMsg = recover() }
    action()
    panicked = false
    return
}

If action() panics, then the line below is never executed, and panicked stays true.
You can check it using the above helper function as follows:

if panicked, msg := checkPanic(func() { /* ... */ }); panicked {
    // handle panic...
} else {
    // happy case :D
}

Or you can directly implement the pattern into the function you want to check, but it might turn out way messier that way.

huangapple
  • 本文由 发表于 2013年10月29日 23:28:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/19662527.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定