Cómo usar Etiquetas de Estructura en Go

Introducción

Las estructuras, o structs, se utilizan para recopilar múltiples piezas de información juntas en una unidad. Estas colecciones de información se utilizan para describir conceptos de nivel superior, como una Dirección compuesta por una Calle, Ciudad, Estado y CódigoPostal. Cuando lees esta información de sistemas como bases de datos o APIs, puedes usar etiquetas de struct para controlar cómo se asigna esta información a los campos de una struct. Las etiquetas de struct son pequeños fragmentos de metadatos adjuntos a los campos de una struct que proporcionan instrucciones a otro código Go que trabaja con la struct.

¿Cómo se ve una Etiqueta de Struct?

Las etiquetas de struct en Go son anotaciones que aparecen después del tipo en una declaración de struct en Go. Cada etiqueta está compuesta por cadenas cortas asociadas con algún valor correspondiente.

A struct tag looks like this, with the tag offset with backtick ` characters:

type User struct {
	Name string `example:"name"`
}

Otro código Go es capaz luego de examinar estas structs y extraer los valores asignados a claves específicas que solicita. Las etiquetas de struct no tienen efecto en el funcionamiento de tu código sin código adicional que las examine.

Prueba este ejemplo para ver cómo se ven las etiquetas de estructura, y que sin código de otro paquete, no tendrán ningún efecto.

package main

import "fmt"

type User struct {
	Name string `example:"name"`
}

func (u *User) String() string {
	return fmt.Sprintf("Hi! My name is %s", u.Name)
}

func main() {
	u := &User{
		Name: "Sammy",
	}

	fmt.Println(u)
}

Esto producirá:

Output
Hi! My name is Sammy

Este ejemplo define un tipo User con un campo Name. El campo Name ha sido asignado con una etiqueta de estructura de example:"name". Nos referiríamos a esta etiqueta específica en conversación como la “etiqueta de estructura de ejemplo” porque usa la palabra “example” como su clave. La etiqueta de estructura example tiene el valor "name" para el campo Name. En el tipo User, también definimos el método String() requerido por la interfaz fmt.Stringer. Esto se llamará automáticamente cuando pasemos el tipo a fmt.Println y nos da la oportunidad de producir una versión bien formateada de nuestra estructura.

Dentro del cuerpo de main, creamos una nueva instancia de nuestro tipo User y la pasamos a fmt.Println. Aunque la estructura tenía una etiqueta de estructura presente, vemos que no tiene ningún efecto en el funcionamiento de este código Go. Se comportará exactamente igual si la etiqueta de estructura no estuviera presente.

Para usar etiquetas de estructura para lograr algo, se debe escribir otro código Go para examinar estructuras en tiempo de ejecución. La biblioteca estándar tiene paquetes que usan etiquetas de estructura como parte de su funcionamiento. El más popular de estos es el paquete encoding/json.

Codificación JSON

JavaScript Object Notation (JSON) es un formato textual para codificar colecciones de datos organizadas bajo diferentes claves de cadena. Comúnmente se utiliza para comunicar datos entre diferentes programas, ya que el formato es lo suficientemente simple como para que existan bibliotecas que lo decodifiquen en muchos lenguajes diferentes. El siguiente es un ejemplo de JSON:

{
  "language": "Go",
  "mascot": "Gopher"
}

Este objeto JSON contiene dos claves, language y mascot. Después de estas claves se encuentran los valores asociados. Aquí la clave language tiene un valor de Go y mascot se asigna el valor Gopher.

El codificador JSON en la biblioteca estándar hace uso de etiquetas de estructura como anotaciones que indican al codificador cómo desea nombrar sus campos en la salida JSON. Estos mecanismos de codificación y decodificación JSON se pueden encontrar en el paquete encoding/json.

Prueba este ejemplo para ver cómo se codifica JSON sin etiquetas de estructura:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string
	Password      string
	PreferredFish []string
	CreatedAt     time.Time
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Esto imprimirá la siguiente salida:

Output
{ "Name": "Sammy the Shark", "Password": "fisharegreat", "CreatedAt": "2019-09-23T15:50:01.203059-04:00" }

Hemos definido una estructura que describe un usuario con campos que incluyen su nombre, contraseña y la hora en que se creó el usuario. Dentro de la función main, creamos una instancia de este usuario proporcionando valores para todos los campos excepto PreferredFish (Sammy le gusta todo tipo de pescado). Luego pasamos la instancia de User a la función json.MarshalIndent. Esto se usa para que podamos ver más fácilmente la salida JSON sin usar una herramienta de formato externa. Esta llamada podría ser reemplazada por json.Marshal(u) para imprimir JSON sin ningún espacio adicional. Los dos argumentos adicionales de json.MarshalIndent controlan el prefijo de la salida (que hemos omitido con la cadena vacía) y los caracteres a utilizar para la indentación, que aquí son dos espacios. Cualquier error producido por json.MarshalIndent se registra y el programa termina usando os.Exit(1). Finalmente, convertimos el []byte devuelto por json.MarshalIndent a una string y pasamos la cadena resultante a fmt.Println para imprimir en la terminal.

Los campos de la estructura aparecen exactamente como se nombran. Sin embargo, este no es el estilo JSON típico que puede esperar, que utiliza mayúsculas y minúsculas para los nombres de los campos. Cambiaremos los nombres de los campos para seguir el estilo camel case en el siguiente ejemplo. Como verá al ejecutar este ejemplo, esto no funcionará porque los nombres de los campos deseados entran en conflicto con las reglas de Go sobre los nombres de campos exportados.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	name          string
	password      string
	preferredFish []string
	createdAt     time.Time
}

func main() {
	u := &User{
		name:      "Sammy the Shark",
		password:  "fisharegreat",
		createdAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Esto presentará la siguiente salida:

Output
{}

En esta versión, hemos modificado los nombres de los campos para que estén en formato camel case. Ahora Name es name, Password es password, y finalmente CreatedAt es createdAt. Dentro del cuerpo de main, hemos cambiado la instanciación de nuestra estructura para usar estos nuevos nombres. Luego pasamos la estructura a la función json.MarshalIndent como antes. La salida, esta vez, es un objeto JSON vacío, {}.

El formateo camel case de los campos correctamente requiere que el primer carácter esté en minúscula. Aunque JSON no se preocupa por cómo se llaman sus campos, Go sí lo hace, ya que indica la visibilidad del campo fuera del paquete. Dado que el paquete encoding/json es un paquete separado del paquete main que estamos utilizando, debemos poner en mayúscula el primer carácter para que sea visible para encoding/json. Parecería que estamos en un callejón sin salida. Necesitamos alguna forma de indicar al codificador JSON cómo nos gustaría que se llamara este campo.

Usando Etiquetas de Estructura para Controlar la Codificación

Puedes modificar el ejemplo anterior para que tenga campos exportados que estén correctamente codificados con nombres de campo en camel case mediante la anotación de cada campo con una etiqueta de estructura. La etiqueta de estructura que reconoce encoding/json tiene una clave de json y un valor que controla la salida. Al colocar la versión en camel case de los nombres de campo como el valor de la clave json, el codificador usará ese nombre en su lugar. Este ejemplo corrige los dos intentos anteriores:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string    `json:"name"`
	Password      string    `json:"password"`
	PreferredFish []string  `json:"preferredFish"`
	CreatedAt     time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Esto producirá:

Output
{ "name": "Sammy the Shark", "password": "fisharegreat", "preferredFish": null, "createdAt": "2019-09-23T18:16:17.57739-04:00" }

Hemos cambiado los campos de la estructura para que sean visibles para otros paquetes al capitalizar las primeras letras de sus nombres. Sin embargo, esta vez hemos agregado etiquetas de estructura en la forma de json:"nombre", donde "nombre" era el nombre que queríamos que json.MarshalIndent usara al imprimir nuestra estructura como JSON.

Ahora hemos formateado correctamente nuestro JSON. Sin embargo, observa que los campos para algunos valores se imprimieron aunque no establecimos esos valores. El codificador JSON también puede eliminar estos campos, si lo prefieres.

Eliminación de Campos JSON Vacíos

Es común suprimir la salida de campos que no están establecidos en JSON. Dado que todos los tipos en Go tienen un “valor cero”, algún valor predeterminado al que están establecidos, el paquete encoding/json necesita información adicional para poder determinar que algún campo debe considerarse no establecido cuando asume este valor cero. Dentro de la parte de valor de cualquier etiqueta json de una estructura, puedes agregar al final el nombre deseado de tu campo con ,omitempty para indicar al codificador JSON que suprima la salida de este campo cuando el campo esté establecido en el valor cero. El siguiente ejemplo corrige los ejemplos anteriores para que ya no se impriman campos vacíos:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name          string    `json:"name"`
	Password      string    `json:"password"`
	PreferredFish []string  `json:"preferredFish,omitempty"`
	CreatedAt     time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Este ejemplo imprimirá:

Output
{ "name": "Sammy the Shark", "password": "fisharegreat", "createdAt": "2019-09-23T18:21:53.863846-04:00" }

Hemos modificado los ejemplos anteriores para que el campo PreferredFish ahora tenga la etiqueta de estructura json:"preferredFish,omitempty". La presencia de la extensión ,omitempty hace que el codificador JSON omita ese campo, ya que decidimos dejarlo no establecido. Esto tenía el valor null en las salidas de nuestros ejemplos anteriores.

Esta salida se ve mucho mejor, pero todavía estamos imprimiendo la contraseña del usuario. El paquete encoding/json proporciona otra manera de ignorar completamente los campos privados.

Ignorar Campos Privados

Algunos campos deben ser exportados de las estructuras para que otros paquetes puedan interactuar correctamente con el tipo. Sin embargo, la naturaleza de estos campos puede ser sensible, por lo que en estas circunstancias, nos gustaría que el codificador JSON ignore completamente el campo, incluso cuando está establecido. Esto se hace usando el valor especial - como argumento de valor para una etiqueta de estructura json:.

Este ejemplo soluciona el problema de exponer la contraseña del usuario.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"
)

type User struct {
	Name      string    `json:"name"`
	Password  string    `json:"-"`
	CreatedAt time.Time `json:"createdAt"`
}

func main() {
	u := &User{
		Name:      "Sammy the Shark",
		Password:  "fisharegreat",
		CreatedAt: time.Now(),
	}

	out, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Println(err)
		os.Exit(1)
	}

	fmt.Println(string(out))
}

Cuando ejecutas este ejemplo, verás esta salida:

Output
{ "name": "Sammy the Shark", "createdAt": "2019-09-23T16:08:21.124481-04:00" }

Lo único que hemos cambiado en este ejemplo respecto a los anteriores es que el campo de contraseña ahora utiliza el valor especial "-" para su etiqueta de estructura json:. En la salida de este ejemplo, el campo password ya no está presente.

Estas características del paquete encoding/json, como ,omitempty, "-", y otras opciones, no son estándares. Lo que un paquete decide hacer con los valores de una etiqueta de estructura depende de su implementación. Debido a que el paquete encoding/json es parte de la biblioteca estándar, otros paquetes también han implementado estas características de la misma manera como cuestión de convención. Sin embargo, es importante leer la documentación de cualquier paquete de terceros que utilice etiquetas de estructura para aprender qué se admite y qué no.

Conclusión

Las etiquetas de struct ofrecen un medio poderoso para mejorar la funcionalidad del código que trabaja con tus structs. Muchas bibliotecas estándar y paquetes de terceros ofrecen formas de personalizar su funcionamiento mediante el uso de etiquetas de struct. Utilizarlas de manera efectiva en tu código proporciona tanto este comportamiento de personalización como documenta de manera sucinta cómo se utilizan estos campos para futuros desarrolladores.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go