Wandering Thoughts archives

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.

programming/GoReflectTypeForOptimization written at 23:12:03;


Page tools: See As Normal.
Search:
Login: Password:

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.