25 May 2023

When 'generic' is too specific

...use a type conversion to prepare for the type assertion!

I've spent a fair bit of effort writing generic functions in Go at this point. I recently wrote a function that received multiple types that served as DTOs ("data transfer objects"). They had no methods and therefore didn't satisfy any interface except the empty interface (interface{}, or as of Go 1.18, any). These types all had a field with the same name and type, which I wanted to return. Something like this: (playground)

func Foo[T any](dto T) uint64 {
	return reflect.ValueOf(dto).FieldByName("Bar").Uint()
}

But generics and reflection in the same function? Yuck, I know, but that's what I reached for when the code I wanted to write didn't compile: (playground)

func Foo[T any](dto T) uint64 {
	switch t := dto.(type) {
	case Thing1:
		return t.Bar
	case Thing2:
		return t.Bar
	}
	return 0
}

The compiler dutifully reports that we cannot use type switch on type parameter value dto (variable of type T constrained by any). Hmm, maybe a type assertion will work? (playground)

func Foo[T any](dto T) uint64 {
	if t, ok := dto.(Thing1); ok {
		return t.Bar
	}
	if t, ok := dto.(Thing2); ok {
		return t.Bar
	}
	return 0
}

Nope, now we get: invalid operation: cannot use type assertion on type parameter value dto (variable of type T constrained by any)

A bit of research reveals that type parameters are special in that they aren't valid 'interface values', which are required by both type assertions and switch statements. So, if we want to use either of those constructs we have to 'convert' our type parameter to an interface value... So, type conversions to the rescue! (playground)

func Foo[T any](dto T) uint64 {
	switch t := any(dto).(type) {
	case Thing1:
		return t.Bar
	case Thing2:
		return t.Bar
	}
	return 0
}

If you're not observant you'll miss the subtle change in the switch statement.

Before:

switch t := dto.(type) {

After:

switch t := any(dto).(type) {

In all my years writing Go code, I don't think I've ever needed to explicitly convert anything to interface{} or any. But that's probably because I've always been able to pass any value to a function that received an interface{} and it just worked. Similarly, I could always just assign a value to a variable or field of type interface{} (or any). So, conversions were mostly for going between different numeric types and things of that nature.

So, the moral of this story is that a type conversion can prepare a generic type parameter for traditional type operations like assertions and switches, or, stated more poetically:

Sometimes even generic code is still too specific!