Índice del contenido

Go con Reflect: Mayor Flexibilidad En Tu Código

Go con Reflect: Mayor Flexibilidad En Tu Código

El paquete “reflect” es una biblioteca útil en el lenguaje de programación Go que proporciona funciones para trabajar con la reflexión.

¿Qué es la reflexión en programación?

La reflexión es la habilidad de un programa de inspeccionar su propia estructura en tiempo de ejecución. Esto incluye la habilidad de examinar: tipos, valores y metada de los objetos en memoria. La reflexión nos permite tener código más flexible y genérico y nos permite crear funciones y estructuras de dato que pueden trabajar con cualquier tipo de objeto, sin importar su tipo. Además es la base para la metaprogramación.

Podemos dividir la reflexión de Go en dos tipos de datos importantes:

  • Type: El tipo de dato de Go, a partir del cual se desglosan todos la información del tipo de dato, su clase, su nombre, tamaño, etc.
  • Value: El valor del dato de Go, con métodos para modificar los datos de un objeto.

Tipo de dato: Type y TypeOf

Type es el tipo de dato más importante en el paquete reflect ya que representa al objeto en memoria y guarda toda la meta información de nuestros objetos: su tipo, representación, métodos y campos.

¿Y como obtenemos un Type? Con el método TypeOf

El método TypeOf del paquete reflect se utiliza para obtener el tipo de una interfaz, recibe un interface (o sea cualquier cosa) y retorna un reflect.Type; “el tipo más importante de reflect”.

graph TD; Reflect-->TypeOf; Reflect-->Type; TypeOf-->Type; Type-->Name; Type-->Kind; Type-->NumField; Type-->Field; Type-->Size;

En este diagrama coloco los que considero los métodos más importantes de Type, pero tanto el tipo reflect.Type y reflect.Value tiene muchos más.

Mira un ejemplo con TypeOf:

type Coffee struct {
	Origin string `tagEjemplo:"valor"`
	Height int
}

instanciaDelCafe := Coffee{Origin: "Michoacan", Height: 1100}
// TypeOf nos devolverá un tipo *reflect.Type*
typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
fmt.Println(typeOfCoffee)
// main.coffee

Observa como el tipo de dato es una estructura que corresponde a main, de nombre coffee.

Name, el nombre del tipo de dato

Name contiene el nombre del respectivo tipo de dato

// TypeOf nos devolverá un tipo *reflect.Type*
typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
fmt.Println(typeOfCoffee.Name())
// coffee

Kind, la clase del tipo de dato

El método TypeOf nos permite obtener un objeto Type, que representa el tipo de dato de nuestra interfaz, a partir del cual podemos leer la clase de dato, con su método Kind.

// TypeOf nos devolverá un tipo *reflect.Type*
typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
fmt.Println(typeOfCoffee.Kind())
// struct

NumField para obtener el número de campos de un struct

A partir de Type podemos obtener todo tipo de información útil, como el número de campos de un objeto. Lo anterior es bastante útil en el caso de structs y otras estructuras con múltiples campos.

// Obtendremos el número de campos de nuestro objeto
typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
coffeeNumFields := typeOfCoffee.NumField()
fmt.Println(coffeeNumFields)
// 2 

En este caso obtenemos 2 campos (Location y Height)

Considera que si intentas obtener el NumField de un tipo int u otro tipo que no cuente con múltiples campos, Go devolverá un error

panic: reflect: Field of non-struct type int

Field y Type anidados de un struct

Type también pone a nuestra disposición el método Field, que junto con NumField, nos dejará acceder directamente a los campos de un struct, a partir de su índice.

Y, con esto, podremos acceder a los Types anidados dentro del objeto Type.

graph TD; TypeDelStruct-->Field; Field--Indice-->TypeDelField; TypeDelField-->Name; TypeDelField-->Kind; TypeDelField-->Otros...;

Cada Type anidado, al igual que el de su padre, contará con los métodos Name y Kind que ya vimos unas lineas arriba.

// Obtendremos el nombre y tipo del primer campo de nuestro objeto
typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
field := typeOfCoffee.Field(0)
fmt.Println(field.Type.Name())
fmt.Println(field.Type.Kind())
// string string

En este caso, al ser tipos de datos primivitos, ambos son el mismo: string; puesto que el campo de tipo string se llama string.

Leer los tags de un campo de un struct con reflect

Reflect también nos permite acceder a los metadatos de un objeto, esas anotaciones que seguramente ya viste si has trabajado con ORM y/o JSON en Go.

Para obtener los Tags de un struct se usa Tag.Get.

// Añadimos un tag personalizado para posteriormente leerlo
type Coffee struct {
	Origin string `tagEjemplo:"valor"`
	Height int
}

typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
field := typeOfCoffee.Field(0)
// ...
fmt.Println(field.Tag.Get("tagEjemplo"))
// valor

Valor del dato: Value y ValueOf

Como te dije anteriormente, además de Type, hay otra parte muy importante en reflect, esta es Value. Value se corresponde con el valor de una interfaz. ¿Y cómo podemos obtener un Value? Pues con ValueOf, ¿te fijas como se conserva la coherencia?

El método ValueOf recibe un interface como parámetro y retorna un objeto de tipo Value que contendrá todos los métodos necesarios para modificar su propio valor.

graph TD; Reflect-->ValueOf; Reflect-->Value; ValueOf-->Value; Value-->Elem; Elem-->Field; Elem-->Set; Elem-->SetString; Elem-->SetComplex; Elem-->SetFloat; Elem-->SetInt; Elem-->SetBool; Elem-->SetInt;

Mira este ejemplo donde se aprecia como ValueOf retorna el valor de nuestro struct.

// ValueOf devolverá el valor de nuestra variable
instanciaDelCafe := Coffee{Origin: "Michoacan", Height: 1100}
ValueOfCoffee := reflect.ValueOf(instanciaDelCafe)
fmt.Println(ValueOfCoffee)
// {Michoacan 1100}

A partir de Value nosotros tendremos una serie de métodos para cambiar el valor de un objeto.

Modificar un campo de un objeto Value

Para modificar un campo necesitamos acceder al objeto Value que apunta a nuestro objeto, el cual podemos obtener pasándole un puntero al método ValueOf. Y, a partir de ahí, usaremos Elem para acceder al valor de nuestro objeto, luego a su primer campo (sólo en el caso de structs) y finalmente cambiar el valor usando SetString.

instanciaDelCafe := Coffee{Origin: "Michoacan", Height: 1100}
valueofCoffee := reflect.ValueOf(&instanciaDelCafe)
valueofCoffee.Elem().Field(0).SetString("Oaxaca")
fmt.Println(instanciaDelCafe)
//{Oaxaca 1100}

Los métodos para asignar datos en reflect

Además del método SetString, Go nos provee de otra serie de métodos especializados para cada tipo de dato :

  • SetBool(x bool)
  • SetBytes(x []byte)
  • SetCap(n int)
  • SetComplex(x complex128)
  • SetFloat(x float64)
  • SetInt(x int64)
  • SetIterKey(iter *MapIter)
  • SetIterValue(iter *MapIter)
  • SetLen(n int)
  • SetMapIndex(key, elem Value)
  • SetPointer(x unsafe.Pointer)
  • SetString(x string)
  • SetUint(x uint64)
  • SetZero()

Estos métodos se pueden acceder directamente desde Value.Elem para tipos de datos no compuestos (como los structs).

stringOriginal := "Yo NO me imprimiré"
// Obtenemos el valor del string
valor := reflect.ValueOf(&stringOriginal)
stringModificado := "Yo SI me imprimiré en la pantalla"
// Cambiamos el valor original del string 
valor.Elem().SetString(stringModificado)
fmt.Printf(stringOriginal)
// Yo SI me imprimiré en la pantalla

Modificar un campo con el método Set

Los métodos especializados para “setear” un dato son muy útiles, pero a veces necesitamos un método más genérico que nos permita modificar los valores de manera dinámica, y para ello reflect nos provee de Set, sí Set a secas, sin nada más.

En este ejemplo, primero creamos un string, para posteriormente obtener el valor de este, con ValueOf, y luego pasarle este nuevo valor al método genérico Set, el cual recibe un objeto Value. ¿y de dónde sacamos Value? Pues de pasarle cualquier dato a ValueOf.

unString := "Oaxaca"
nuevoValue := reflect.ValueOf(unString)
valueofCoffee.Elem().Field(0).Set(nuevoValue)
// {Oaxaca 1100}

Creación de un objeto con reflect

Reflect también nos permite crear objetos de manera dinámica a partir de un Type. Para lo anterior, basta pasarle como argumento el tipo de objeto, Type, que queremos crear, al método New, el cual, como ya sabes, lo podemos obtener fácilmente a partir de TypeOf

typeOfCoffee := reflect.TypeOf(instanciaDelCafe)
nuevaInstancia := reflect.New(typeOfCoffee)
fmt.Println(nuevaInstancia)
// &{ 0}

A partir del tipo que obtuvimos con TypeOf reflect creará un nuevo objeto. Sin embargo, en este caso, al tratarse de un struct al cual no le hemos especificado los valores de sus campos, se inicializará con sus zero values.

Y ya se acabó la pequeña introducción a reflect, ahora ya sabes lo suficiente para confundirte y llorar mientras trabajas con cualquier cosa que necesite reflexión. ¿Genial no?

Eduardo Zepeda
Desarrollador web, entusiasta de los sistemas GNU/Linux y el Software Libre. Py, Ts y Go, pero abierto a otras opciones como el Rustaceanismo. Creo en las bondades de las criptodivisas más allá de la especulación monetaria.
Leer más