Reflection and embedded types

  2018-09-03


As part of developing gaum I found myself in the need to take apart an instance of a struct to populate a call to Scan with the instance’s attributes mapped to the sql row columns.

When addressing a struct in the code, ie:

// BaseType is a poorly name type that exemplifies a struct to be embedded
type BaseType struct {
    aField int64
}

type OtherType struct {
    BaseType

    anotherField string
}

Now, in your code you should be able to simply invoke aField in an instance of OtherType by referencing it directly like so:

var example OtherType
example.aField

Sadly, reflection does not think like that, when reflecting over the tyle OtherType you will see to fields:

  • anotherField of type string
  • and an Anonymous field of type BaseType

Now, if you think that you can declare OtherType in this manner:

example := OtherType{
    BaseType: BaseType{
        aField: 1,
    },
    anotherField: "a vaue",
}

And reference it by calling example.BaseType.aField it does make some sense.

So, for my case where I was trying to map a set of sql column names to struct fields I ended up making a recursive unwrapper function that makes a map of sql serialized field names to struct fields, if a key is found to be repeated the struct field is discarded because in the common use the outer most fields shadow the inner embedded ones.

func unwrapEmbedded(fields map[string]reflect.StructField, anonfield *reflect.StructField) {
	tod := anonfield.Type
	embeddedFields := []*reflect.StructField{}
	var ok bool
	for fieldIndex := 0; fieldIndex < tod.NumField(); fieldIndex++ {
		field := tod.Field(fieldIndex)
		if field.Anonymous {
			embeddedFields = append(embeddedFields, &field)
			continue
		}

		name := nameFromTagOrName(field)
		// the assumption that are no conflicting fields is made, if there were conflicting fields
		// the user will most likely get a complain about ambiguous identifier before this or
		// upon scanning next, it is too risky to try to recreate what the compiler would do.
		_, ok = fields[name]
		if !ok {
			fields[name] = field
		}
	}

	if len(embeddedFields) != 0 {
		for _, v := range embeddedFields {
			unwrapEmbedded(fields, v)
		}
	}
}

One last thing that bit me while trying to correct this is that I was using this map to then get the fields by index and had to switch to doing it by name because doing it this way makes seamless retrieval of attributes like in regular go syntax.

So I had to change

fVal in this context is reflect.StructField from the above map

vod in this context is reflect.Value of an instance of the type we unwrapped.

fieldRecipients[i] = vod.FieldByIndex(fVal.Index).Addr().Interface()

becomes

fieldRecipients[i] = vod.FieldByName(fVal.Name).Addr().Interface()
comments powered by Disqus