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)

Most upvoted comments

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):

  1. v.Type() == nil // untyped
  2. v.IsNil() == true // untyped nil
  3. v.IsZero() == true // any nil is a zero value, so probably the untyped nil is too

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?

I wonder, though, what would actually break if reflect.ValueOf(nil).IsNil() did not panic.

That was my thought too.

I think it would probably be fine if:

  • reflect.ValueOf(nil) returns the zero value of Value (as it does now)
  • for the zero value of Value, IsNil returns true
  • for the zero value of Value, Type returns nil.

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 Value as roughly equivalent to an untyped nil, then I propose the following comprehensive changes.

Changes for cases where v is the zero Value:

  • func (v Value) IsNil() bool

    Reports true. This address the original proposal.

  • func (v Value) IsValid() bool

    No change; returns false. Documentation will be updated to say that it reports whether v is a valid typed value.

  • func (v Value) Kind() Kind

    No change; returns 0 (which is the Invalid kind). We could create an alias renaming Invalid as UntypedNil.

  • func (v Value) IsZero() bool

    Reports true. Rationale: reflect.ValueOf([1]T{nil}).Index(0).IsZero reports true when T is a chan, func, interface, map, pointer, or slice kind.

  • func (v Value) Convert(t Type) Value

    Returns a typed nil value if t.Kind is a chan, func, interface, map, pointer, or slice. Rationale: T(nil) is valid Go where T is one of the above kinds.

  • func (v Value) CanConvert(t Type) bool

    Reports true if t.Kind is 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() == nil

  • func (v Value) CanInterface() bool

    Reports true. This is to preserve round-trip behavior such that ValueOf(nil).Interface() == nil

  • func (v Value) UnsafePointer() unsafe.Pointer

    Returns nil. This change is debatable.

  • func (v Value) Pointer() uintptr

    Returns 0. This change is debatable.

  • func (v Value) String() string

    No change; this continues to return <invalid reflect.Value>. The reasonable alternative is to return nil or <nil>, but that will almost certainly break many tests that assume this never changes.

  • func (v Value) Type() Type

    No change; this continues to panic. The reasonable alternative is to return nil, but then we will need to figure out how Make and New functions interact with a nil Type.

  • func Indirect(v Value) Value

    No change; this already accepts a zero Value.

Changes for cases where v is not a zero Value:

  • func (v Value) Set(x Value)

    If v.CanSet and v.Kind is a chan, func, interface, map, pointer, or slice, and x is a zero Value, then this is equivalent to v.Set(Zero(v.Type())). Rationale: var x T = nil is valid Go where T is one of the above kinds.

    Note that v cannot be zero for the same reason that nil = x is not valid Go.

    This addresses #52310.

  • func Append(v Value, x ...Value) Value

    Assuming v is a valid slice and v.Elem.Kind is a chan, func, interface, map, pointer, or slice value, then elements of x may be a zero Value, in which case they are appended as typed nil values. Rationale: v = append(v, nil) is valid Go.

    Note that v cannot be zero for the same reason that v = append(nil) is not valid Go.

  • func AppendSlice(v, t Value) Value

    Assuming v is a valid slice, then t may be a zero Value, in which case v is returned as is. Rationale: v = append(v, nil...) is valid Go.

    Note that v cannot be zero for the same reason that v = append(nil) is not valid Go.

ValueOf returns a representation of the value boxed in the argument interface, instead of the argument interface itself. A nil interface boxes nothing. Maybe nothing should not be nil?

package main

import "reflect"

func main() {
	var v any
	y := reflect.ValueOf(&v).Elem()
	println(y.IsNil()) // true
}