Is there a way to use reflect to discern type-relationships in Go?

234 Views Asked by At

Context

Writing a URL Query Parameter parser for Go language library

Problem

Only structs have a form of inheritance in Go, that I am aware of. One can use the reflect package to discern the kind of an entity, i.e. it's fundamental storage class within the type system, and one can probe the type of said element. So I can discern that the entity is of Kind string, and Type Title, as an arbitrary example, assuming something like this exists:

type Title string

Better, for structs, I can use anonymous members to gain a limited type of inheritance:

type Foo struct { name string }
func (f Foo) Hello() string { return f.name; }

type Bar struct { Foo }

func main() {
  b := Bar{ Foo{"Simon"} }
  fmt.Println(b.Hello())
}

Live example

The point being, Go allows me to extend a Foo as Bar but inherit / reuse Foo functions at least for the portion that is a Foo.

However, for a non-struct type - I am unaware as to how to approach this problem for an encoding/decoding library similar to json or xml - where I want to be able to decode query params into a struct's members, and crucially, be able to support user-defined types without requiring that every one of them defines a specialized decoder or encoder for my purposes, so long as they're derivatives of a type that supports an interface that I can utilize.

In the concrete, I would like to support user-defined types that are stored as a Google uuid.UUID, or a time.Time (or time.Duration would be useful as well).

Here's a simplistic example:

type TransactionID uuid.UUID

As declared, this inherits zero behaviors of uuid.UUID, and I'm not sure if, or how, to use reflect to intentionally make this happen in my encoder/decoder library?

Go itself won't provide any sort of inheritance or relationship between them. My TransactionID is NOT a uuid.UUID, but they share the same Kind - a [16]byte array, and to the best of my current knowledge, that is all they share.

This is my conundrum

How to allow a user defined type that I wish to support w/o requiring my users now define those same encoder or decoder functions for their type that I've already defined for the "fundamental" type?

Some More Context

My decoder library makes this interface available:

// Parser is an interface for types that RequestParser can parse from a URL parameter
type Parser interface {
    ParseParameter(value string) error
}

And I have defined a specialized extension for uuid.UUID and time.Time and a few other useful non-struct types to decode them from query parameter strings. If the entity type I'm decoding into is literally a uuid.UUID or a time.Time and not a user-defined type based on those, then things work properly. Similarly, user aliases work because they're not genuine new types.

Aliases are too limited for my purposes - they do no provide any meaningful distinction from the aliased type, which severely limits their actual utility. I will not respond to suggestions that require their use instead. Thank you.

UPDATE

Ideally, a user should be able to define TransactionID for their purpose and I would like to know that it is a "kind of" UUID and hence - by default - use a UUID parser.

It is already trivially possible for a user to define:

func (id *TransactionID) ParseParameter(value string) (err error) {
   id_, err := uuid.Parse(value)
   if err != nil {
      return
   }
   *id = TransactionID(id_)
   return
}

This will force my decoder to choose their well-defined interface - and decode the input exactly as they wish for their type.

What I wish - is to have a way to know that their type is a derivative type of ____ what?, and if I have a parser for a ____ what - then -- in the absence of a user-defined ParseParameter -- use my default supplied smart one (which knows that a [16]byte is not the same thing as a UUID, and understands more sophisticated ways to decode a UUID).

Addendum

I will expand this question and provide more details as I get thoughtful and useful responses. I'm not sure at this point what else I might provide?

Thank you for taking the time to thoughtfully respond, should you choose to do so.

1

There are 1 best solutions below

2
On BEST ANSWER

It is not possible to use the reflect API to discern that one type was created from another type.

The specification says:

A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.

and

The new type is called a defined type. It is different from any other type, including the type it is created from.

The specification does not define any other relationship between the defined type and the type it was created from. There is not a "created from" relationship for the reflect API to expose.

In the example

type A B

the reflect API cannot tell you that type A was created from type B because there is no relationship between type A and type B except for the shared underlying type.

Type conversions from A to B and B to A are allowed because A and B have identical underlying types.

You can use the reflect API to determine if two types have identical underlying types, but that does not help with the problem. It could be that A was created from B, B was created from A or neither was created from from the other. In this example, type A and B have identical underlying types:

 type A int
 type B int

The question states that the parser has a builtin decoder for uuid.UID. The goal is to use that parser for type TransactionID uuid.UUID. Here are some ways to solve that problem:

Embedding Declare the type as type TransactionID struct { uuid.UID }. Check for struct with single known embedded field and handle accordingly.

Registry Add function function to register mappings between convertible types.

 var decodeAs  = map[reflect.Type]reflect.Type{}
 
 func RegisterDecodeAs(target, decode reflect.Type) {
     if !decode.ConvertibleTo(target) {
        panic("type must be convertible")
     }
     decodeAs[target] = decode
 }

When decoding, check for decode type in decodeAs. If present, decode to the decoding type, convert to the target type and assign.

Call the registration function at program startup:

 func init() {
     RegisterDecodeAs(reflect.TypeOf(TransactionID(nil)), reflect.TypeOf(uuid.UUID(nil))
 }