Identify non builtin-types using reflect

Some background

First let’s clear some things related to types. Quoting from Spec: Types:

A type determines the set of values and operations specific to values of that type. Types may be named or unnamed. Named types are specified by a (possibly qualified) type name; unnamed types are specified using a type literal, which composes a new type from existing types.

So there are (predeclared) named types such as string, int etc, and you may also create new named types using type declarations (which involves the type keyword) such as type MyInt int. And there are unnamed types which are the result of a type literal (applied to / including named or unnamed types) such as []int, struct{i int}, *int etc.

You can get the name of a named type using the Type.Name() method, which “returns an empty string for unnamed types”:

var i int = 2
fmt.Printf("%q\n", reflect.TypeOf("abc").Name())              // Named: "string"
fmt.Printf("%q\n", reflect.TypeOf(int(2)).Name())             // Named: "int"
fmt.Printf("%q\n", reflect.TypeOf([]int{}).Name())            // Unnamed: ""
fmt.Printf("%q\n", reflect.TypeOf(struct{ i int }{}).Name())  // Unnamed: ""
fmt.Printf("%q\n", reflect.TypeOf(&struct{ i int }{}).Name()) // Unnamed: ""
fmt.Printf("%q\n", reflect.TypeOf(&i).Name())                 // Unnamed: ""

There are types which are predeclared and are ready for you to use them (either as-is, or in type literals):

Named instances of the boolean, numeric, and string types are predeclared. Composite types—array, struct, pointer, function, interface, slice, map, and channel types—may be constructed using type literals.

Predeclared types are:

bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr

You may use Type.PkgPath() to get a named type’s package path, which “if the type was predeclared (string, error) or unnamed (*T, struct{}, []int), the package path will be the empty string”:

fmt.Printf("%q\n", reflect.TypeOf("abc").PkgPath())    // Predeclared: ""
fmt.Printf("%q\n", reflect.TypeOf(A{}).PkgPath())      // Named: "main"
fmt.Printf("%q\n", reflect.TypeOf([]byte{}).PkgPath()) // Unnamed: ""

So you have 2 tools available to you: Type.Name() to tell if the type is a named type, and Type.PkgPath() to tell if the type is not predeclared and is a named type.

But care must be taken. If you use your own, named type in a type literal to construct a new type (e.g. []A), that will be an unnamed type (if you don’t use the type keyword to construct a new, named type):

type ASlice []A

fmt.Printf("%q\n", reflect.TypeOf([]A{}).PkgPath())    // Also unnamed: ""
fmt.Printf("%q\n", reflect.TypeOf(ASlice{}).PkgPath()) // Named: "main"

What can you do in such cases? You may use Type.Elem() to get the type’s element type, if type’s Kind is Array, Chan, Map, Ptr, or Slice (else Type.Elem() panics):

fmt.Printf("%q\n", reflect.TypeOf([]A{}).Elem().Name())    // Element type: "A"
fmt.Printf("%q\n", reflect.TypeOf([]A{}).Elem().PkgPath()) // Which is named, so: "main"

Summary

Type.PkgPath() can be used to “filter out” predeclared and unnamed types. If PkgPath() returns a non-empty string, you can be sure it’s a “custom” type. If it returns an empty string, it still may be an unnamed type (in which case Type.Name() returns "") constructed from a “custom” type; for that you may use Type.Elem() to see if it is constructed from a “custom” type, which may have to be applied recursively:

// [][]A -> Elem() -> []A which is still unnamed: ""
fmt.Printf("%q\n", reflect.TypeOf([][]A{}).Elem().PkgPath())

// [][]A -> Elem() -> []A -> Elem() -> A which is named: "main"
fmt.Printf("%q\n", reflect.TypeOf([][]A{}).Elem().Elem().PkgPath())

Try all the examples on the Go Playground.

Special case #1: Anonymous struct types

There is also the case of an anonymous struct type which is unnamed, but it may have a field of a “custom” type. This case can be handled by iterating over the fields of the struct type and performing the same check on each field, and if any of them is found to be a “custom” type, we can claim the whole struct type to be “custom”.

Special case #2: Map types

In case of maps we may consider an unnamed map type “custom” if any of its key or value type is “custom”.

The value type of a map can be queried with the above mentioned Type.Elem() method, and the key type of a map can be queried with the Type.Key() method – we also have to check this in case of maps.

Example implementation

func isCustom(t reflect.Type) bool {
    if t.PkgPath() != "" {
        return true
    }

    if k := t.Kind(); k == reflect.Array || k == reflect.Chan || k == reflect.Map ||
        k == reflect.Ptr || k == reflect.Slice {
        return isCustom(t.Elem()) || k == reflect.Map && isCustom(t.Key())
    } else if k == reflect.Struct {
        for i := t.NumField() - 1; i >= 0; i-- {
            if isCustom(t.Field(i).Type) {
                return true
            }
        }
    }

    return false
}

Testing it (try it on the Go Playground):

type K int
var i int = 2
fmt.Println(isCustom(reflect.TypeOf("")))                // false
fmt.Println(isCustom(reflect.TypeOf(int(2))))            // false
fmt.Println(isCustom(reflect.TypeOf([]int{})))           // false
fmt.Println(isCustom(reflect.TypeOf(struct{ i int }{}))) // false
fmt.Println(isCustom(reflect.TypeOf(&i)))                // false
fmt.Println(isCustom(reflect.TypeOf(map[string]int{})))  // false
fmt.Println(isCustom(reflect.TypeOf(A{})))               // true
fmt.Println(isCustom(reflect.TypeOf(&A{})))              // true
fmt.Println(isCustom(reflect.TypeOf([]A{})))             // true
fmt.Println(isCustom(reflect.TypeOf([][]A{})))           // true
fmt.Println(isCustom(reflect.TypeOf(struct{ a A }{})))   // true
fmt.Println(isCustom(reflect.TypeOf(map[K]int{})))       // true
fmt.Println(isCustom(reflect.TypeOf(map[string]K{})))    // true

Leave a Comment