go
go copied to clipboard
proposal: spec: guarantee non-nil return value from recover
Calling panic with a nil panic value is allowed in Go 1, but weird.
Almost all code checks for panics with:
defer func() {
if e := recover(); e != nil { ... }
}()
... which is not correct in the case of panic(nil).
The proper way is more like:
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:
package runtime
type nilPanic struct{}
// NilPanic is the value returned by recover when code panics with a nil value.
var NilPanic nilPanic
Probably Go2.
Dear runtime,
if I perform panic(nil) or panic(somethingThatCanBeNil), it may be a mistake. That's my problem. But I may also do that intentionally and with the magically changed value, I need to not forget about that and workaround the magic.
Less the magic, the less exceptional rules I have to think about. It makes me more productive and my code more comprehensible to my future self. Thanks.
edit: typo
An alternative solution would be to allow recover() to be used in ,ok assignments. For that to really solve the stated problem though, that would require all call sites to be updated.
My personal leaning at the moment is in favor of the proposal as stated.
Is any program out there using nil panics intentionally? A simple search for panic(nil) doesn't give anything on my entire GOPATH besides a go/ssa/interp test. But I'm more worried about panics with variables that could/would be nil.
In any case, I agree with the sentiment and the proposed solution. Perhaps runtime.NilPanic should be clarified that it's only for untyped nils, though. For example, this case has a nil value but wouldn't be equal to nil when recovered:
var err *myError
panic(err)
FTR: I admit this is a real problem. I just prefer explicitly handling it.
Is this a real problem, though? I doubt it. What if the implementation of panic with a nil argument instead just did, panic("nil value passed to panic")? Thereby fixing the problem and diagnosing it one one stroke.
Is this a real problem, though? I doubt it.
I've had to deal with it at least twice. net/http had hangs when people panicked with nil.
@mpvl was talking about error handling the other day and was showing some examples of how defensive code should ideally look like (and how it's hard to get right), and he forgot the nil panic case, showing it's even harder than it seems.
What if the implementation of panic with a nil argument instead just did, panic("nil value passed to panic")? Thereby fixing the problem and diagnosing it one one stroke.
That's what I'm proposing, except with a variable (which could have a String method with that text). But I'm fine (but less fine) with it being that string value exactly, as matching on strings is a little gross.
This proposal makes total sense--for a language which prides itself on its simplicity and obviousness, it is perplexing that the only way to check for a panic(nil) in a recover is by using some other variable. I can't possibly think up a situation when someone would call panic(nil) on purpose anyhow.
While I believe that this is a very good thing to be able to handle, I think that a , ok pattern would be a bit better. If I panic(nil) then in a language like Go, I would want nil to be the recover() value.
So then recover would look like:
defer func() {
if e, ok := recover(); ok {
// do recovery stuff
}
}();
@deanveloper I like your idea for being backwards compatible.
The proper way is more like: […]
That turns out not to be correct either: runtime.Goexit can terminate the goroutine without a recoverable value, but that pattern (and the variant as written by @cznic) would incorrectly report a panic where no panic occurred.
(https://play.golang.org/p/yS1A1c5csrR)
As far as I can tell, there is no way for a defer call to distinguish between panic(nil) and runtime.Goexit(), which to me suggests that at least one of them should be removed.
As far as I can tell, there is no way for a
defercall to distinguish betweenpanic(nil)andruntime.Goexit(), which to me suggests that at least one of them should be removed.
As it turns out, we can use the fact that a runtime.Goexit() cannot be recover'd to distinguish it from panic(nil), via a “double defer sandwich”:
https://play.golang.org/p/nlYYWPRO720
Change https://golang.org/cl/134395 mentions this issue: errgroup: rethink concurrency patterns
I think we should treat panic(nil) as a runtime error, so it should panic with a value that implements the runtime.Error interface. This is less convenient for people who want to explicitly detect panic(nil), but I don't see why that matters; if you want to check for it, then, instead, don't do it.
if you want to check for it, then, instead, don't do it.
Perhaps you're using a badly-written library, and you don't have control over it. I've seen worse before :man_shrugging:
Our team spent 3 hours today hunting down what we now know to be a panic(nil) call. The problem is that panic(nil) is valid yet undetectable since recover() only returns the value that was panic-ed.
This proposal as-is would work as the common if recover() != nil { pattern would see a non-nil value. Therefore, has my vote. (Some internal type would work too, as long as it is not nil)
Making recover() return two values (the panic-ed value and whether a panic occurred) as mentioned several times would make more sense, However, changing recover() will undoubtedly be rejected for being non-backward compatible. Or too complicated if made magical (like map and range that provide one or two values, depending how you use it).
Sometimes. people may call panic(nil) for success. See the case 3 in this article for an example.
[edit] in this example, the panic value is not nil, but it can be.
@go101, eliminating code like that would be an added bonus.
I agree this pattern is some weird. But I think panic(nil) should be treated as an unexpected feature.
Another use case (also some weird, ;D):
func doSomething() (err error) {
defer func() {
err = recover()
}()
doStep1()
doStep2()
doStep3()
doStep4()
doStep5()
return
}
// panic with nil for success and no needs to continue.
// panic with error for failure and no needs to contine.
// not panic to continue.
func doStepN() {...}
which is much less verbose than
func doSomething() (err error) {
shouldContinue, err := doStep1()
if !shouldContinue {
return err
}
shouldContinue, err = doStep2()
if !shouldContinue {
return err
}
shouldContinue, err = doStep3()
if !shouldContinue {
return err
}
shouldContinue, err = doStep4()
if !shouldContinue {
return err
}
shouldContinue, err = doStep5()
if !shouldContinue {
return err
}
return
}
// if err is not nil, then shouldContinue must be true.
func doStepN() (shouldContinue bool, err error) {...}
@go101, this is quickly going off topic. All that code might be amusing to some, but it's not code we want to enable or encourage.
I agree.
The proposal as-is could be conditionalized on the Go version specified in the go.mod file, to be be fully backwards-compatible.
@neild, the go version specified in the go.mod file applies to individual packages, not the binary as a whole. What happens if a go 1.13 package passes nil as an interface{} to a go 1.15 package, which then invokes panic with that as its argument?
@bcmills Simplest approach I can think of: panic always converts nil to runtime.NilPanic. In a pre-go1.15 package, recover converts it back into nil.
So if a go1.13 package passes (interface{})(nil) to a go1.15 package which panics, that's the same as panic(runtime.NilPanic). However, if a go1.13 package recovers from that panic, it sees nil.
I came across the problem of panic(nil) the other day. I think I generally like both proposals here. Independent of those, however, I'd also propose we include checking panic values as a go vet check. I suspect a common reason this would occur in the wild is a copy/paste mistake like
x, err := doThing()
if err != nil {
panic(err)
}
if x == nil {
panic(err)
}
I've seen code along these lines before, when dealing with odd cases where both return values of a function might be nil in some case. The second panic was copied to quickly handle x being nil after realizing the possibility, but not taking care to switch to a new value to panic with.
While doing one of the proposals above would be great for robustness, I think a vet check would help catch the simple mistakes like this one. I've got a sketched out change that adds it as a step to the existing package nilness. It seems like a good fit: it already built up all the requisite facts, so the work was just looking at the panics. The tests I've written seem like it detects the common cases I'd care about, as well.
The change is here: https://github.com/stephens2424/tools/commit/3817b808a2d7b6515559b16a088aa978aa7d8598
If the idea sounds good, I'll tidy it up into a real CL.
@stephens2424 seems promising. If it isn't too much work, please do mail a CL.
Change https://golang.org/cl/220777 mentions this issue: go/analysis/passes/nilness: detecting panic with provably nil values
I've uploaded the change, cleaned up a bit and with a real commit message. In the past few days, I've realized a couple points, however.
First, as is, this change will not be included by go vet. I see some information that suggests it might be available in golangci-lint, and less convincingly, in gopls and some odd copy of go vet. (As an aside, the difference between importers in godoc.org and pkg.go.dev is interesting.) I gather that the reason this analysis pass is not in go vet is because the SSA form is too expensive to compute, and I doubt this new feature adds enough value to tip that scale. I think this new feature makes sense to exist in the pass, though, and it'll get picked up by tools if they do decide to add this analyzer.
Second, it occurred to me that, if it's practical, reversing this analyzer, making it much more strict, could be an interesting alternative approach. That is to say: the analyzer would require that panic values be provably non-nil, requiring authors to add specific checks when there's any ambiguity. If it works, I think it could be a viable solution to this issue on its own, or possibly with some attention to getting wider adoption of the analyzer. I'm still new to working with the SSA form, though, so my experimentation with the idea is a little slow going, and I might be getting ahead of myself with this even being possible. Fun to learn it though! It seems very powerful!
I have a slightly ugly suggestion for this. Add a new builtin:
func recover2() (v interface{}, ok bool)
This is a backward compatible change, and provides a way for newly written code to be able to check whether a panic(nil) happened.
Edit to add more exposition:
I don't think it's possible to retroactively fix existing code using recover/panic "incorrectly" without breaking the backward compatibility promise. It's perfectly valid according to the Go spec to write code that deliberately calls panic(nil) and handles that case accordingly with recover() != nil. It might be in poor taste, but the behavior is unambiguously defined in the spec. We cannot change this unless we guarantee that no code has ever done this (impossible) or break users of well-defined Go behavior (really bad PR for the backward compatibility promise).
I think this case is similar to time.Timer.Reset, whose return value cannot be used correctly. A new recommendation was added, but we didn't try to retroactively fix broken code.
@darkfeline Why add recover2 instead of just allowing err, ok := recover()?
@mdempsky I'm not sure that's possible, given that recover is a normal function, not a keyword. If it is possible, then allowing recover() to have one or two return values depending on context would be better, yes.
Edit: See https://github.com/golang/go/blob/master/src/runtime/panic.go#L1080 I think it'd be difficult to overload recover().