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 untypednilvariables.
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 .)