Get type parameter from a generic struct using reflection
Asked Answered
L

4

9

Imagine I have the following struct:

type MyGeneric[T string | int] struct {
}

I want to check whether the generic used to instantiate that struct was a string or a int when creating a new MyGeneric.

myGenericString := MyGeneric[string]{}
myGenericString.canHandle("hello") -> should return true
myGenericString.canHandle(8) -> should return false

func (mG MyGeneric[T]) canHandle(value any) bool {
    // how to get what T is the same type as value
}

Leukoderma answered 27/9, 2022 at 8:45 Comment(0)
T
14

For normal values, just instantiate the T directly to get its value and reflect.TypeOf() it. But we can declare a [0]T and take its element type instead, because:

  • [0]T takes 0 memory while T may be as large as T, which would waste a lot of stack and heap 1 if T is something like [4096]int.
  • A raw T does not work with interface types. reflect.TypeOf() on a nil interface value (whether empty interface or not) will return reflect.Type(nil), of which subsequent uses will cause panic.
package main

import (
    "fmt"
    "reflect"
)

type MyGeneric[T any] struct {
}

func (mG MyGeneric[T]) canHandle(value any) bool {
    var zero [0]T
    tt := reflect.TypeOf(zero).Elem()
    vt := reflect.TypeOf(value)

    fmt.Printf("-> %v covers %v\n", tt, vt)
    return vt.AssignableTo(tt)
}

type empty struct{}

func main() {
    fmt.Printf("%v\n", MyGeneric[string]{}.canHandle(""))
    fmt.Printf("%v\n", MyGeneric[any]{}.canHandle(""))
    fmt.Printf("%v\n", MyGeneric[string]{}.canHandle(1))
    fmt.Printf("%v\n", MyGeneric[MyGeneric[struct{}]]{}.canHandle(MyGeneric[struct{}]{}))
    fmt.Printf("%v\n", MyGeneric[MyGeneric[struct{}]]{}.canHandle(MyGeneric[empty]{}))
}

Output:

-> string covers string
true
-> interface {} covers string
true
-> string covers int
false
-> main.MyGeneric[struct {}] covers main.MyGeneric[struct {}]
true
-> main.MyGeneric[struct {}] covers main.MyGeneric[main.empty]
false

1 Not sure if it can be optimized out or whether it allocates on heap or stack or both, because it is passed into reflect.TypeOf after upcasting to an interface{}.

Toffee answered 3/10, 2022 at 7:27 Comment(3)
note that this solution works unless T is instantiated with interfaces.Shammy
@Shammy it works if you use the [0]T method.Toffee
Updated the answer to prioritize the [0]T instead of T.Toffee
S
6

It hasn't been implemented yet. There is an open proposal about adding the necessary methods to reflect.Type.

The current workaround as of Go 1.19 is to parse the string obtained from TypeOf. Something like this:

var r = regexp.MustCompile("[A-Za-z0-9_]+\\.[A-Za-z0-9_]+\\[(.*)\\]")

func (mG MyGeneric[T]) typeParam(value any) {
    tname := reflect.TypeOf(mG).String() // this is `main.MyGeneric[string]`
    match := r.FindStringSubmatch(tname)
    fmt.Println(match[1]) // string
}

This if the goal is just to obtain the name of the type parameter. It's not great, as it depends on the type's string representation. On the bright side, it doesn't force you to think about what happens if you instantiate T with interfaces.

If you need to do further computations with the type of T, e.g. compare it to other types etc. @SOFe’s answer provides a solution that doesn’t depend on arbitrary string representations.

However watch out for T instantiated with interfaces: see also In Golang, how to compare interface as generics type to nil?

Shammy answered 27/9, 2022 at 9:28 Comment(0)
A
4

Another solution is to add a placeHolder field to your struct, that can be used to get its type via type switch to avoid reflect:

type MyGeneric[T string | int] struct {
  placeHolder T
}

func (mG MyGeneric[T]) canHandle(value any) bool {
    switch t1 := any(p.placeHolder).(type) {
    case any:
        switch t2 := value.(type) {
        case any:
            return t1 == t2
        }
    }
    return false
}
Alienage answered 27/9, 2022 at 10:15 Comment(0)
B
1

Simple casting seems to work (go 1.23)

func (t *MyGeneric[T]) canHandle(v any) bool {
    _, ok := v.(T)
    return ok
}
Brindle answered 16/9 at 7:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.