go: runtime: Goexit call will cancel a panic, which seems unexpected/incorrect
In go 1.13, but probably any version of go, a runtime.Goexit call can cancel a panic (example from @go101):
package main
import "runtime"
func main() {
c := make(chan struct{})
go func() {
defer close(c)
// The Goexit signal shadows the
// "bye" panic, but it should not.
defer runtime.Goexit()
panic("bye")
}()
<-c
}
When you run this program, instead of getting a panic that terminates the program, you get a normal termination with no error. And generally, when a goroutine is panicking, a call to runtime.Goexit will just end the goroutine, but will cancel the panic, so the whole program may continue running.
This is not a big deal, since it has been like this for a long time and people are unlikely to run into it, but seems unexpected/incorrect.
One solution would be just to say that runtime.Goexit() is a no-op if we are in the middle of a panicking sequence. The slight downside to such a solution is that turning runtime.Goexit() to a no-op might be surprising and violate some programmer assumptions (especially if the runtime.Goexit call is buried deep within some other function/module), maybe leading to a second panic.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 15 (10 by maintainers)
I just realized things are not so simple. Goexit sometimes behaves like panic, sometimes not.
The final decision really twists the explanations for panic/recover mechanism to some degree.
Yes, Goexit is unrecoverable painc.
I mean those 3 functions are equivalent if they are used as goroutine start functions.
@bcmills and I spent a little while thinking and talking through this today.
To me, if a deferred handler calls os.Exit in a panic, clearly the process exits and basically cancels the panic (there is no panic dump etc). So if a deferred handler calls runtime.Goexit in a panic, it seems OK to me that the goroutine exits “cleanly” rather than preserving the panic. That is, Goexit cancels a panic.
On the other hand it’s clear that panic/recover should not cancel Goexit, which was #35377 and is fixed.
Based on this logic and trying some test cases, @bcmills and I believe this issue is working as intended and can be closed.
The doc for
runtime.Goexit
says, front and center, “Goexit terminates the goroutine that calls it.” Given that, I don’t think it can reasonably fail to terminate the goroutine, even if it looks like the goroutine might end up terminating anyway.And if
runtime.Goexit
could fail to exit the goroutine, I would expect it to have a different name that indicates the actual behavior.We should immediately end the defer that called
Goexit
, the same as we would do if the deferred call invokedpanic
.That I do not know. To my knowledge we haven’t specified what the stack looks like during a
Goexit
, but in general I would expect the top frame to have an accurateGoexit
call frame. If there are chunks missing (due to arecover
sequence recovering them), I would expect the missing chunk(s) to be indicated somehow.I double-checked the behavior in a couple of Playground programs.
https://go.dev/play/p/Adxb5z9XBvO looks reasonable to me: the
runtime.Goexit
cancels the panic that was in flight, just like how anos.Exit
would equivalently cancel apanic
from the same goroutine, a subsequent panic can be initiated and recovered within the deferred calls, and recovering the panic doesn’t cancel theGoexit
.https://go.dev/play/p/vmSAgH-DZgF looks wrong, but in the opposite direction from the title of this issue. The
from before Goexit
panic should be cleared by theGoexit
, but when the program terminates it somehow resurfaces in the stack dump.