go: proposal: reflect: reflect.ValueOf(nil).IsNil() panics; let's make it return true
The IsNil method of reflect.Value applies only to typed nils, so this program panics:
package main
import "reflect"
func main() {
x := reflect.ValueOf(nil)
x.IsNil()
}
panic: reflect: call of reflect.Value.IsNil on zero Value
On the face of it, this is nuts: it says that nil is not nil. (What is this, a NaN? Just joking.) In fact, even asking if nil is nil causes the program to crash!
Every time I dig into uses of reflect, which would be rare except that I “own” several core packages that depend on reflection, I bounce off this. It just sits wrong with me that reflect.ValueOf(nil) gives the zero reflect.Value, but that is “invalid” not nil.
I propose, without thinking through the consequences in nearly enough detail, that either “invalid” becomes different from “value created from literal nil”, or that we just change “invalid” to “nil” and allow it to satisfy IsNil.
I’m not sure this can be changed without causing a major ruckus, but I thought I’d at least ask. It might be easy and could clean up a fair bit of code in some places.
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 26
- Comments: 19 (18 by maintainers)
The big problem here is that reflect is about typed values yet in many use cases people want some representation for untyped nil. And reflect.ValueOf(nil) returns the zero reflect.Value, which people use as that representation.
So suppose we make the following changes to support that (assume v is the zero reflect.Value):
It’s a little inconsistent but it will fix some code and probably won’t break much, since all these panic today and code is written to avoid those panics.
Does anyone object to this?
That was my thought too.
I think it would probably be fine if:
In general a nil Type represents “no type” in the same way that the zero Value represents “no value”, and nil is the nearest we’ve got in Go to a generic way to represent “nothing”, so IsNil returning true seems reasonable to me.
I wonder if there’s any code at all that would break if this behaviour were to change.
If we treat a zero
Valueas roughly equivalent to an untypednil, then I propose the following comprehensive changes.Changes for cases where
vis the zeroValue:func (v Value) IsNil() boolReports true. This address the original proposal.
func (v Value) IsValid() boolNo change; returns false. Documentation will be updated to say that it reports whether
vis a valid typed value.func (v Value) Kind() KindNo change; returns 0 (which is the
Invalidkind). We could create an alias renamingInvalidasUntypedNil.func (v Value) IsZero() boolReports true. Rationale:
reflect.ValueOf([1]T{nil}).Index(0).IsZeroreports true whenTis a chan, func, interface, map, pointer, or slice kind.func (v Value) Convert(t Type) ValueReturns a typed
nilvalue ift.Kindis a chan, func, interface, map, pointer, or slice. Rationale:T(nil)is valid Go whereTis one of the above kinds.func (v Value) CanConvert(t Type) boolReports true if
t.Kindis a chan, func, interface, map, pointer, or slice.func (v Value) Interface() (i any)Returns nil. This is to preserve round-trip behavior such that
ValueOf(nil).Interface() == nilfunc (v Value) CanInterface() boolReports true. This is to preserve round-trip behavior such that
ValueOf(nil).Interface() == nilfunc (v Value) UnsafePointer() unsafe.PointerReturns nil. This change is debatable.
func (v Value) Pointer() uintptrReturns 0. This change is debatable.
func (v Value) String() stringNo change; this continues to return
<invalid reflect.Value>. The reasonable alternative is to returnnilor<nil>, but that will almost certainly break many tests that assume this never changes.func (v Value) Type() TypeNo change; this continues to panic. The reasonable alternative is to return
nil, but then we will need to figure out howMakeandNewfunctions interact with anilType.func Indirect(v Value) ValueNo change; this already accepts a zero
Value.Changes for cases where
vis not a zeroValue:func (v Value) Set(x Value)If
v.CanSetandv.Kindis a chan, func, interface, map, pointer, or slice, andxis a zeroValue, then this is equivalent tov.Set(Zero(v.Type())). Rationale:var x T = nilis valid Go whereTis one of the above kinds.Note that
vcannot be zero for the same reason thatnil = xis not valid Go.This addresses #52310.
func Append(v Value, x ...Value) ValueAssuming
vis a valid slice andv.Elem.Kindis a chan, func, interface, map, pointer, or slice value, then elements ofxmay be a zeroValue, in which case they are appended as typed nil values. Rationale:v = append(v, nil)is valid Go.Note that
vcannot be zero for the same reason thatv = append(nil)is not valid Go.func AppendSlice(v, t Value) ValueAssuming
vis a valid slice, thentmay be a zeroValue, in which casevis returned as is. Rationale:v = append(v, nil...)is valid Go.Note that
vcannot be zero for the same reason thatv = append(nil)is not valid Go.ValueOfreturns a representation of the value boxed in the argument interface, instead of the argument interface itself. Anilinterface boxes nothing. Maybe nothing should not be nil?