>

¿Cómo usar reflection en Go?

Friends of Go     Colaboraciones    16/05/2019

Este artículo ha sido escrito por Adrián Pérez y publicado originalmente en el blog de Friends of Go

¿Qué es reflection? Reflection es la habilidad que tiene un programa para examinar y modificar su propia estructura y comportamiento en tiempo de ejecución. El propósito que tiene reflection es la de permitir a los programadores crear código genérico, además es la clave para poder realizar metaprogramación

Cada lenguaje tiene su propia forma de realizar reflection y Go no iba a ser menos, pero cada forma de aplicarlo es distinta ya que ésta se utiliza sobre el sistema de tipos del propio lenguaje.

Dado que Go es un lenguaje de tipado estático, es decir, la comprobación del tipado se realiza durante la compilación y no durante la ejecución, permitiendo que los errores de tipado sean detectados antes y que la ejecución del programa sea mucho más eficiente y segura, y que además nosotros definimos de que tipo es cada variable durante la programación, podríamos llegar a pensar que reflection en Go podría no tener sentido.

Pero esa afirmación no es cierta, ya que podríamos querer obtener cierta información que no conocemos de antemano, pero no nos quedemos en palabrería vamos a ver cómo se resuelve reflection en GO

golang reflection

El paquete reflect

Como casi siempre que hablamos de una funcionalidad del lenguaje, tenemos un paquete detrás que nos facilita la tarea de como enfrentarnos a dicha funcionalidad, en el caso de reflection no iba a ser menos y Go nos da el paquete reflect.

Antes de entrar en materia queremos destacar, que se asume que sabes lo que haces cuando utilizas dicho paquete, porque muchos de los métodos que nos ofrece el paquete si no son bien usados pueden acabar en panic.

La clave de reflection en Go

Para entender la clave de reflection en Go tendremos que entender cómo funcionan, Type, Kind y Value.

Como siempre lo veremos con ejemplos prácticos.

reflection 1

El resultado de ejecutar este código es el siguiente:

main.Gopher
struct
{Adam Blue 20}

Playground: https://play.golang.org/p/oZsAi_xfzV7

En el anterior código vemos tres métodos que pertenecen al paquete reflect vamos a explicar qué hace cada uno.

  • reflect.TypeOf() espera un interface{} como parámetro y nos devuelve un reflect.Type es decir conoceremos el tipo de la interfaz que le estamos pasando, en este caso un Gopher.
  • Kind() a partir de un reflect.Type sabremos la clase de tipo que es, en este caso struct.
  • reflect.ValueOf() dado un interface{} podremos averiguar su valor.

Hay que tener en cuenta que aunque utilizando el fmt.Println(rValue) nos devuelve el valor de nuestra struct éste no podrá ser utilizado sin ser previamente casteado, ya que realmente no es un Gopher lo que devuelve sino un reflect.Value. Veámoslo mejor en un ejemplo:

...
name := rValue.Name
fmt.Println(name)
...

Si ejecutamos el código anterior tendremos un error como éste:

prog.go:18:16: rValue.Name undefined (type reflect.Value has no field or method Name)

Pero podemos realizar el casteo correspondiente y poder acceder a los métodos y propiedades de nuestro struct

...
name := rValue.Interface().(Gopher).Name
fmt.Println(name)
...

Inspeccionando con reflection

Reflection nos permite hacer todavía más, podemos llegar a obtener información de cuántos campos tiene una struct y de que tipo es cada campo. Si la variable es un slice, channel, puntero, map o array, también nos ofrece métodos para poder examinar su tipo y su contenido.

Vamos a ver cómo poner todo esto en práctica con un ejemplo.

reflection 2

El resultado de la ejecución es:

Type: Gopher Kind: struct
Type: main.Gopher Kind: ptr

Playground: https://play.golang.org/p/8M7e_aRhMJZ

En el ejemplo anterior ya empezamos a ver algunas diferencias con lo que vimos previavemente, y es que la forma que tiene de comportarse reflection si es un struct o es otro de los tipos mencionados al principio del artículo es relativamente diferente.

Podemos ver que cuando es un struct podemos llamar al método Name() que ofrece reflect.Type, y éste nos devuelve el nombre de nuestro struct pero si hiciéramos eso con nuestro acceso a memoria posterior obtendríamos un string vacío, así que tenemos que utilizar el método Elem() el cual ya si que nos dará la información.

Antes también comentamos que podíamos conocer el número de argumentos de un struct y conocer el tipo de cada campo que lo compone, ¿cómo lo hacemos?

reflection 3

El resultado de la ejecución es:

Field Type: Name: string Kind: string
Field Type: Color: string Kind: string
Field Type: Year: int Kind: int

Playground: https://play.golang.org/p/maDvcoIY4r6

Pues sí, como vemos un método tan simple como NumField() nos permite acceder al número de elementos que se compone nuestro struct a través de su reflect.Type, una vez tenemos eso simplemente podemos acceder a ellos mediante un índice y el método Field(i int) el cual nos devuelve un reflect.Value.

Vemos también que a la hora de imprimir su información tenemos que acceder a través del atributo Type el cual ya nos ofrece, su tipo(Type) y su clase(Kind), además el atributo Name nos permite conocer el mismo del atributo en cuestión de nuestro struct.

Pero podemos llegar aún más lejos, en la inspección incluso podríamos conocer los tags que tienen nuestros atributos.

...
if field.Tag != "" {
    fmt.Printf("Tag json: %s\n", field.Tag.Get("json"))
}
...

Si volvemos a ejecutar nuestro script añadiendo ese bloque if, tendremos el siguiente resultado (también hay que modificar el struct añadiendo el tag):

Tag json: color,omitempty

Playground: https://play.golang.org/p/_-MQ9aUKF8U

Incluso podríamos ver como acceder a la información de otro struct si uno de nuestros atributos es de tipo struct. Simplemente tendríamos que comprobar que el atributo sea de Kind() struct, field.Type.Kind() == reflect.Struct y recorrerlo igual que hacemos con el struct principal.

Modificando mediante reflection

Al principio del artículo comentabamos que además de inspeccionar podríamos modificar, nuestras variables, y además os adelanto que podemos tambien crear en tiempo de ejecución nuevas instancias de ellas.

Para poder modificar nuestras variables, simplemente necesitaremos un puntero hacia ellas y el paquete reflect nos dará todo lo necesario para lo demás.

reflection 4

Si ejecutamos esta pieza de código obtendremos el siguiente resultado:

{Adam Blue 20}
I'm a gopher, and I will be modified via reflection
**********************
{Killgrave Purple 55}
Modified string
**********************
Hi I'm Brian, my color is Red and I've 34 years

Playground: https://play.golang.org/p/b_dZqAtyv2o

Aunque por el ejemplo puede llegar a intimidar, realmente hemos introducido pocas cosas nuevas, vamos a analizar el código.

Como comentábamos para poder modificar nuestras variables, lo primero que necesitaremos será punteros a ellas y además obtener sus reflect.Value respectivos. Una vez tenemos los reflect.Value simplemente tendremos que modificarlos utilizando para ello Elem().Set(v Value) es decir necesitaremos un reflect.Value con nuestro nuevo valor, pero si es un tipo básico vemos que esto no es necesario ya que el paquete reflect nos ofrece métodos como SetString, SetInt, SetBool, entre otros.

Así pues una vez volvemos a imprimir nuestras variables, veremos qué tienen los nuevos datos que les hemos seteado.

Pero ahí no queda la magia sino que a través del reflect.New podremos crear una instancia completamente nueva a partir de un reflect.Type e inicilizar sus valores como hemos aprendido anteriormente, después solo tendremos que convertir nuestro reflect en el tipo que necesitamos, y ¡voilà, un mundo de posibilidades se abre ante nosotros!

Conclusión

Hemos dado unas pinceladas a las posibilidades que ofrece reflection, aún queda mucho más por profundizar y aprender, pero ya tenéis una base para poder empezar a utilizar esta gran utilidad, aunque como bien dijo Ben Parker, en su día un gran poder conlleva una gran responsabilidad y es que usar reflection es un poco delicado, recordemos que se usa en tiempo de ejecución con lo cual que nuestro código compile no quiere decir que no podamos encontrarnos errores a la hora ejecutarlo, y recordemos en muchos casos incluso podremos obtener panic.

Así que utiliza reflection con mucho cuidado y siempre que de verdad lo necesites, piensa que muchas veces que creas que necesitas utilizar reflection para solucionar tu problema posiblemente sea porque lo estes planteando mal, o estes pensando como hacer las cosas como en algún lenguaje no tipado.

Recuerda que si tienes cualquier duda o sugerencia, puedes dejarlo en los comentarios o vía nuestro twitter FriendsOfGo