2024-02-14
Understanding a recent optimization to Go's reflect.TypeFor
Go's reflect.TypeFor()
is
a generic function that returns the reflect.Type
for its type argument. It was
added in Go 1.22, and its initial implementation was quite simple
but still valuable, because it encapsulated
a complicated bit of reflect
usage.
Here is that implementation:
func TypeFor[T any]() Type { return TypeOf((*T)(nil)).Elem() }
How this works is that it constructs a nil pointer value of the
type 'pointer to T', gets the reflect.Type
of that pointer,
and then uses Type.Elem() to go from the pointer's Type to the Type
for T itself. This requires constructing and using this 'pointer
to T' type (and its reflect.Type
) even though we only what
the reflect.Type
of T itself. All of this is necessary for
reasons to do with interface types.
Recently, reflect.TypeFor()
was optimized a bit, in CL 555597,
"optimize TypeFor for non-interface types". The code for
this optimization is a bit tricky and I had to stare at it for a
while to understand what it was doing and how it worked. Here is
the new version, which starts with the new optimization and ends
with the old code:
func TypeFor[T any]() Type { var v T if t := TypeOf(v); t != nil { return t } return TypeOf((*T)(nil)).Elem() }
What this does is optimize for the case where you're using TypeFor()
on a non-interface type, for example 'reflect.TypeFor[int64]()
'
(although you're more likely to use this with more complex things
like struct types). When T is a non-interface type, we don't need
to construct a pointer to a value of the type; we can directly
obtain the Type from reflect.TypeOf
. But how do we tell whether or
not T is an interface type? The answer turns out to be right there
in the documentation for reflect.TypeOf
:
[...] If [TypeOf's argument] is a nil interface value, TypeOf returns nil.
So what the new code does is construct a zero value of type T, pass
it to TypeOf(), and check what it gets back. If type T is an interface
type, its zero value is a nil interface and TypeOf() will return
nil; otherwise, the return value is the reflect.Type
of the
non-interface type T.
The reason that reflect.TypeOf
returns nil for a nil interface
value is because it has to. In Go, nil
is only sort of typed, so if a nil interface value is passed to
TypeOf(), there is effectively no type information available for
it; its old interface type is lost when it was converted to 'any
',
also known as the empty interface. So all
TypeOf() can return for such a value is the nil result of 'this
effectively has no useful type information'.
Incidentally, the TypeFor() code is also another illustration of
how in Go, interfaces create a difference between two sorts of
nils. Consider calling 'reflect.TypeFor[*os.File]()
'.
Since this is a pointer type, the zero value 'v
' in TypeFor() is
a nil pointer. But os.File
isn't
an interface type, so TypeOf() won't be passed a nil interface and
can return a Type, even though the underlying value in the interface
that TypeOf() receives is a nil pointer.