Sunday, May 29, 2016

Simpler Golang reflection

It's not often that I need Golang reflection, but when I do... I always hated it. In Java you write something like obj.GetClass(), in Python it's obj.__class__ and you can get everything you need from there. And classes are basically descriptors for everything you can encounter in runtime (except primitives in Java).

In Golang, for every "thing", the medatata describing it is type. But there is also kind which sometimes you need to consult to get the real underlying type (if the kind is a pointer).

To get a list of fields, you need to check if it is a struct or a pointer to a struct. Then, when getting a list of fields, you must check if a field is anonymous or not. That's why you need a recursion to get the field list, but in that recursion some field names can happen twice (because they are declared in different anonymous structs).

Then to get/set a field by name, you need to check if the original is a struct or a pointer to a struct (again). If it is a struct you get the field value with ...FieldByName(fieldName) if it is a pointer it's ...Elem().FieldByName(fieldName),  but (only) for values you can use reflect.Indirect().  For every field you have two metadata types describing it: reflect.Value and reflect.StructField. In Java, when you have a reference to Field, you have everything you need to get/set/describe that field.

There is also a weirdness with nil values -- any nil value is not really nil in memory, but a nil with a type metadata. You can call (some) methods on nil values. So, in a way they behave like normal values, but you can't get/set their fields.

In short Java and Python reflection is much simpler than Golang reflection. At least, for me it is.

But, since sometimes I really had to do this reflection thing, I decided to simplify it as much as possible and created go-reflector. You basically give him any value, and it gives you a simple API to work with fields, methods and tags.

Example usage:

import "github.com/tkrajina/go-reflector/reflector"

p := Person{}
// Initializing:
obj := reflector.New(p)

// Fields:
obj.Field("Name").IsValid()
val, err := obj.Field("Name").Get()
err := obj.Field("Name").Set("Something")

// Tags:
jsonTag := obj.Field("Name").Tag("json")
jsonTag := obj.Field("Name").TagExpanded("json")
fieldTagsMap := obj.Field("Name").Tags()

// Listing fields:
fields := obj.FieldsAll()
fields := obj.FieldsFlattened()
fields := obj.Fields()
doubleDeclaredFields := obj.FindDoubleFields()
if len(doubleDeclaredFields) > 0 {
    fmt.Println("Detected multiple fields with same name:", doubleDeclaredFields)
}

// Calling methods:
resp, err := obj.Method("Hi").Call("John", "Smith")
if err != nil {
    fmt.Println("Cannot call method:", err.Error())
}
if resp.IsError() {
    fmt.Println("Method returned an error:", resp.Error.Error())
} else {
    fmt.Println("Method call response:", resp.Result)
}

// Get all methods:
for _, method := range obj.Methods() {
    fmt.Println("Method", method.Name(), "with input types", method.InTypes(), "and output types", method.OutTypes())
}