perri.to: Un mejunje de cosas

Reflection y tipos embebidos

  2018-09-03


Como parte del desarrollo de gaum me encontré con la necesidad de descomponer una instancia de un struct para poblar una llamada a Scan con los atributos de la instancia mapeados a las columnas de la fila de sql.

Cuando uno se refiere a un struct en el código, es decir:

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

type OtherType struct {
    BaseType

    anotherField string
}

Ahora bien, en tu código deberías poder simplemente invocar aField en una instancia de OtherType referenciándolo directamente así:

var example OtherType
example.aField

Lamentablemente, reflection no piensa de esa manera, cuando reflexionás sobre el tipo OtherType vas a ver dos campos:

  • anotherField de tipo string
  • y un campo Anonymous de tipo BaseType

Ahora, si pensás que podés declarar OtherType de esta manera:

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

Y referenciarlo llamando a example.BaseType.aField, tiene bastante sentido.

Así que, para mi caso donde estaba tratando de mapear un conjunto de nombres de columnas de sql a campos de struct, terminé haciendo una función recursiva de desempaquetado que arma un mapa de nombres de campos serializados de sql a campos de struct; si se encuentra una clave repetida, el campo del struct se descarta porque, en el uso común, los campos más externos opacan a los embebidos internos.

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)
		}
	}
}

Una última cosa que me hizo tropezar mientras intentaba corregir esto es que estaba usando este mapa para después obtener los campos por índice y tuve que cambiar a hacerlo por nombre, porque hacerlo de esta forma permite una recuperación transparente de los atributos como en la sintaxis normal de go.

Así que tuve que cambiar

fVal en este contexto es reflect.StructField del mapa de arriba

vod en este contexto es reflect.Value de una instancia del tipo que desempaquetamos.

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

se convierte en

fieldRecipients[i] = vod.FieldByName(fVal.Name).Addr().Interface()