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)

Most upvoted comments

I recommend to implement Goexit as harmless panic

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.

I don’t really see why a deferred call to Goexit should linger after the call to panic.

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.

When the runtime.Goexit is called (after a panic has happened), do we continuing executing normal code after the Goexit, or do we immediately end the defer that the Goexit was running in and continue to the next defer in the panicking sequence?

We should immediately end the defer that called Goexit, the same as we would do if the deferred call invoked panic.

If all panics are recovered, and we return to processing the Goexit, what does the stack look like at that point in runtime.Callers(). Does the stack have to look exactly like what it was when the Goexit was first called?

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 accurate Goexit call frame. If there are chunks missing (due to a recover 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 an os.Exit would equivalently cancel a panic from the same goroutine, a subsequent panic can be initiated and recovered within the deferred calls, and recovering the panic doesn’t cancel the Goexit.

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 the Goexit, but when the program terminates it somehow resurfaces in the stack dump.