Nil in Go is typed in theory and sort of untyped in practice
I was recently reading Down the Golang nil Rabbit Hole (via), and ran into the statement part way through:
So here’s a fun fact, Go has multiple “types” of
nil
. Specifically, there are both typed and untypednil
variables.
Some of you reading this are now reaching for your keyboards (cf), and that was my first reaction too. But
I think it's more interesting to talk about how someone could wind
up feeling that nil
is sometimes untyped, because Go's nil
is
a little bit slippery when you combine it with other features and
a necessary API for some things. So let's start with what nil
is in Go and how it behaves in some circumstances.
In code, a literal of 'nil
' effectively works as if it was an
untyped constant like '0' (although it's not a constant; it's a
predeclared identifier
with special semantics that are sprinkled all through the
specification). However, just like
untyped constants, when you use nil
by assigning it to a variable
or passing it as a function argument, the resulting Go value is
always typed, whether it is a pointer, function, slice, map, channel,
or interface of some type.
For concrete types that can be nil
(pointers, functions, slices,
maps, and channels), the type of even these nil
values is readily
observed. The fmt
package's %T
verb will report it, for example, and you can examine this type
through reflect
. However this
is not quite the case for interfaces.
Like most everything else in Go, interfaces are typed, including
the famous empty interface, interface{}
, and as a result so are
interface values. An interface value that is nil
still has a type.
However, this interface type (of interface values) is very hard to
observe dynamically (in code) because of another property of Go:
when you convert one interface type to another interface type,
the original interface type is lost. If an interface value is not
nil
, it has an underlying concrete type, which is preserved when
the interface value is converted to another interface type (although
the original interface type is lost). But if the interface value
is nil
, there's no underlying concrete type and so there's no
other type information to be preserved; you can't tell a nil
of
one interface type from a nil
of another interface type.
Now we get to the subtle Go trap. Neither fmt
nor reflect
have a magic exemption from the Go type system in order to accept
arbitrary Go types. Instead, they must use the Go escape hatch of
the empty interface, interface{}
, which everything can be converted
to. But this means that when you call fmt
or reflect
on an
interface value, you're performing an interface conversion, and
as a result you lose the type of the original interface value. For
instance, suppose you write code like this:
func fred(e error) { fmt.Printf("%#v %T\n", e, e) }
The error
type is an interface type. When you call fmt.Printf
,
the error
interface value is converted to the interface{}
type, and fmt.Printf
now has no access to the fact that e
was
an error
; all it can report is the concrete underlying type, if
there is one. If you call 'fred(nil)
', fmt
winds up being passed
a nil
value of type interface{}
and can only shrug about what
type that value originally was.
(By extension, the fmt
'%T
' verb can never tell you what interface
type was being used. If you want to know that, you must pass a
pointer to the interface value and then '%T
' that, which will
report things like '*error
'. See this playground example, which demonstrates that
this does preserve the interface type for a nil
interface value,
as you'd expect.)
In this sense, nil
interface values are untyped, in that their
type is very hard to observe dynamically and check in practice in
ordinary Go code. Due to this, a lot of perfectly normal Go code
will tell you that such values have no type (fmt
's '%T
' verb
will report their type as '<nil>
', for example).
(Perhaps their type should be reported as 'interface{}
', but that
ship has sailed and it wouldn't be all that much more useful in
practice.)
The other way to put this is that anything that takes interface{}
arguments can't distinguish between nil
values of different
interface types, and by extension also can't distinguish between
called with a literal nil
and being called with an interface value
that happens to be nil
(for example, because it's a value of
(interface) type error
and there was no error). Both fmt
and
reflect
take interface{}
arguments and so are limited by this.
If you're not aware of this indistinguishability, things can look
pretty puzzling (or you can get caught out by it).
(This is another facet of how sometimes a nil
is not a nil
, which is sufficiently common to have a FAQ about
it.)
|
|