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.Goexitsays, 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.Goexitcould 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 accurateGoexitcall frame. If there are chunks missing (due to arecoversequence 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.Goexitcancels the panic that was in flight, just like how anos.Exitwould equivalently cancel apanicfrom 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 Goexitpanic should be cleared by theGoexit, but when the program terminates it somehow resurfaces in the stack dump.