Introducci贸n
Nota: Los materiales de ejemplo de este art铆culo se pueden encontrar aqui
Siguiendo con la tem谩tica del post anterior vamos a utilizar un poco de actualidad financiera local (de Argentina) para ejemplificar uno de los usos comunes de go, en este caso vamos a utilizar una API http que devuelve resultados en JSON que des-serializaremos a diferentes representaciones en Go para pdoer trabajar con ellos.
El ejercicio que realizaremos ser谩 consultar la api p煤blica de Mercado Libre para hacer una b煤squeda del tel茅fono mas caro que hay en el mercado en este momento un iPhone 11 Max Pro que en su lugar de “origen” (Estados Unidos) Tiene un costo de U$D1499 mas impuestos en su versi贸n mas cara (El modelo con 512G de almacenamiento). Me pareci贸 simpatico hacer una aplicaci贸n que nos diga cuanto cuesta en la plaza local, para hacer esto hice un poco de trampa y hago una b煤squeda por dicho tel茅fono y la ordeno de mas caro a mas barato y tomo el mas caro, seguramente en el mismo sitio se encuentren algunos mas baratos.
Para simplificar la explicaci贸n vamos a separar las funcionalidades en funciones individuales que explicaremos por partes.
La petici贸n de consulta
Lo primero que necesitamos hacer es la petici贸n HTTP a la API de Mercado Libre, para ello vamos a crear la funci贸n queryML que realizar谩 la petici贸n y nos devolver谩 un io.ReadCloser correspondiente al cuerpo de la respuesta (el JSON) o un error si no se puede realizar la petici贸n correctamente.
Nuestra petici贸n ser谩 una acci贸n de GET que ademas de la URL deber谩 tener una cadena de consulta con los par谩metros que requiere esta API para realizar la b煤squeda en cuesti贸n, tener en cuenta que, de tener un par谩metro equivocado, la API devuelve un resultado equivalente a una b煤squeda sin resultados.
Deberemo pasar un parametro para el ordenamiento, contenido en sortKey y uno para la b煤squeda, contenido en queryKey. Para asegurarnos de construir una URL v谩lida convertiremos el string que la contiene en un objeto net/url.URL parte de las librerias que vienen con el lenguaje, luego le pediremos un diccionario con las claves/valores de la cadena de consulta (que en este caso estar谩 vac铆a dado que la URL de base no contiene ning煤n parametro). Modificaremos el mapa de valores obtenido y lo volveremos a asignar al objeto url.URL para luego re-generar un string con una URL v谩lida.
const iPhone11Max = "iPhone 11 Pro Max"
const (
// Si quieren buscar en otro lado reemplazaran MLA por su pais
baseMeLiURL = "https://api.mercadolibre.com/sites/MLA/search"
queryKey = "q"
sortKey = "sort"
sortID = "price_desc"
)
/* ... */
func queryML() (io.ReadCloser, error) {
// convertimos la URL de base a un objeto url.URL
queryURL, err := url.Parse(baseMeLiURL)
if err != nil {
return nil, fmt.Errorf("parsing mercado libre url: %v", err)
}
// Obtenemos un diccionario de clave/valor de los parametros de GET
queryValues := queryURL.Query()
// Agregamos los parametros que nos interesan
// Ordenar por mas caro primero
queryValues[sortKey] = []string{sortID}
// Criterio de b煤squda: un tel茅fono car铆simo
queryValues[queryKey] = []string{iPhone11Max}
// Re-asignamos el diccionario de valores a la query original.
queryURL.RawQuery = queryValues.Encode()
// Realizamos la consulta con el string de URL v谩lido generado por queryURL.
response, err := http.Get(queryURL.String())
if err != nil {
return nil, fmt.Errorf("querying mercado libre url: %v", err)
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("requesting to mercado libre: %s", response.Status)
}
return response.Body, nil
}La respuesta, parseando JSON a un diccionario de Go
Hay mas de una forma de interpretar un resultado en JSON para convertirlo a datos de Go, veremos dos a continuaci贸n, de mas gen茅rica a mas puntual.
Disponiendo del io.ReadCloser que nos da acceso al cuerpo de la respuesta, lo primero que debemos hacer es recordar encolar una llamada a Close() para que se ejecute al salir de la funci贸n actual y as铆 ya es un asunto resuelto, esto es opcional pero recomendado.
Interface
Antes de seguir, deberiamos hablar brevemente de las interfaces, en Go, una interface nos permite definir, a grandes rasgos, una variable especificando su comportamiento en lugar de su tipo. Un buen ejemplo de esto pudimos verlo en io.ReadCloser que es el tipo de Body, que en vez de aludir especificamente al tipo que tendra lo que se asign贸 a Body nos dice que espera que implemente el m茅todo Read([]byte) (int, error) y el m茅todo Close() error justamente especificando una interface que es la mezcla de io.Reader e io.Closer.
Las interfaces nos permiten ser expl铆citos en la funcionalidad m铆nima que esperamos de un tipo y as铆 permitir en una variable o funci贸n el uso de varios tipos concretos que cumplan con esos requisitos.
A falta de soporte para programacion gen茅rica en el lenguaje, a veces necesitamos aplicar transformaciones a un objeto contenido en una variable del tipo de una interface para devolverlo a su tipo concreto si es que necesitamos algo, esto se puede lograr de dos maneras simples que veremos mas adelante, estas transformaciones se llama “aserciones de tipo” que no deben confundirse con “casteo” de variables dado que lo que se hace es realizar aserciones de la interface contra un tipo concreto para ver si el objeto en memoria es en efecto perteneciente al tipo, no hay conversi贸n.
Una variante, no muy recomendada, es la interface vacia, que nos permite definir una interface con la que cualquier tipo coincide por el simple hecho de que no tiene ningun requerimiento m铆nimo, se define como interface {} indicando con las llaves vac铆as que no se espera ningun atributo. En el caso que veremos a continuaci贸n, se utilizar谩 para de-serializar un JSON dado que en el caso de cargar JSON a primitivas como mapas no podemos definir en Go uno que soporte tipos mixtos como valor (en los mapas de Go definimos el tipo de la clave y del valor pero estos aplican a todos los elementos del mapa)
De-serializaci贸n a mapas
La naturaleza de JSON hace que tenga algo de fricci贸n a la hora de intentar cargarlo en estructuras r铆gidas. A un nivel muy bajo, un objeto JSON es un mapa o diccionario de claves y valores que a su vez pueden ser otros mapas, listas o primitivas. Una forma simple de intentar cargar uno es la que veremos en el ejemplo a continuaci贸n: definiremos un mapa map[string]interface{} que esperar谩 que todas sus claves sean strings y aceptar谩 cualquier cosa como valor, luego deberemos recorrerlo tomando los valores en las claves que nos interesan e intentar hacer “aserciones de tipo” con cada una de ellas. Vemos como primero obtenemos los results que es una lista de objetos as铆 que hacemos una aserci贸n a un []interface{} que es como estan almacenados y luego recorremos cada uno de sus elementos y hacemos aserci贸n a map[string]interface{} dado que sabemos que cada uno de ellos es un mapa. Notemos que en cada aserci贸n se utiliza la notaci贸n espec铆fica <interface>.(tipoEsperado) y que se esperan dos resultados, el objeto de tipo concreto y una variable booleana que nos indica si la aserci贸n fue exitosa. Podemos omitir la variable booleana pero en caso de no haber 茅xito se disparar铆a un panic
func iPhoneMasCaroML() (decimal.Decimal, error) {
// obtendremos el cuerpo de la respuesta de la funci贸n queryML, que es un io.ReadCloser
body, err := queryML()
if err != nil {
return decimal.Zero, err
}
// recordaremos cerrar el cuerpo al finalizar
defer body.Close()
// leemos todo el cuerpo en un arreglo de bytes, no es recomendable hacerlo de esta manera
// en c贸digo de producci贸n ya que no estamos chequeando el largo del contenido antes de
// guardarlo en memoria, si nuestro c贸digo hiciese muchas de estas llamadas posiblemente
// ocuparia mucha memoria.
bodyData, err := ioutil.ReadAll(body)
if err != nil {
return decimal.Zero, fmt.Errorf("reading mercado libre response body: %v", err)
}
// de-serializamos el contenido del cuerpo a un map[string]interface{}
resultML := map[string]interface{}{}
err = json.Unmarshal(bodyData, &resultML)
if err != nil {
return decimal.Zero, fmt.Errorf("unmarshaling mercado libre response body: %v", err)
}
// buscamos en el map, la clave de la lista de resultados
resultsRaw, ok := resultML[resultsKey]
if !ok {
return decimal.Zero, fmt.Errorf("key %s not found in response JSON", resultsKey)
}
// convertimos de un objeto interface{} a un []interface para poder utilizar las
// caracter铆sticas de una lista
results, ok := resultsRaw.([]interface{})
if !ok {
return decimal.Zero, fmt.Errorf("unexpected results type %T", resultsRaw)
}
// chequeamos que, ademas de ser una lita, tenga en efecto resultados.
if len(results) == 0 {
return decimal.Zero, fmt.Errorf("nobody is selling an %s", iPhone11Max)
}
// obtenemos el primer resultado que, dado el ordenamiento de mas caro a mas barato
// deber铆a ser el mas caro.
resultRaw := results[0]
// convertimos este resultado nuevamente a un tipo que podamos manipular.
result, ok := resultRaw.(map[string]interface{})
if !ok {
return decimal.Zero, fmt.Errorf("unexpected single type %T", resultRaw)
}
// buscamos la clave del precio
priceRaw, ok := result[priceKey]
if !ok {
return decimal.Zero, fmt.Errorf("price is not available")
}Luego de varias aserciones exitosas tenemos priceRaw que de momento es interface{} que sabemos contiene el precio, pero no sabemos si es un int, un string o un float para esto utilizaremos una aserci贸n de tipo combinada con un switch.
Este es un caso especial en que la condici贸n presentada al switch es variable := variableInterface.(type) donde type es la palabra clave type lo que provocara que el intento de coincidencia con cada casese haga en el tipo, cuando coincida el tipo, dentro del 谩mbito de ese case la variable tendra el tipo concreto que coincidi贸 y entonces podremos operar con funciones que tomen ese tipo concreto o asignar a variables, en el ejemplo a continuaci贸n vemos como convertimos el valor de priceRaw a decimal.Decimal dependiendo de que tipo sea.
En la realidad sabemos que es float64 pero puede darse el caso de que sea un resultado mas variado dada la naturaleza no tipada de JSON.
Si ningun caso coincide devolveremos un error por que no podremos convertir el valor a decimal y por ende operar con el mismo.
// utilizamos type switch para convertir el precio a decimal desde varios tipos
// posibles.
var moneyPrice decimal.Decimal
switch price := priceRaw.(type) {
case float64:
moneyPrice = decimal.NewFromFloat(price)
case float32:
moneyPrice = decimal.NewFromFloat32(price)
case string:
moneyPrice, err = decimal.NewFromString(price)
if err != nil {
return decimal.Zero, fmt.Errorf("cannot translate price to a decimal value: %v", err)
}
default:
return decimal.Zero, fmt.Errorf("price is not a type we can convert to decimal, is %T", priceRaw)
}
return moneyPrice, nil
}De-serializaci贸n a tipos
No solo podemos deserializar a mapas y de esta forma tan procedural, Go nos permite tambi茅n de-serializar a tipos concretos. Podemos definir uno o varios tipos que representen el total o una parte del contenido del JSON y a traves de anotaciones en el c贸digo utilizar variables de esos tipos para de-serializar el JSON. En este caso si el tipo tiene mas miembros que el JSON se dejaran en su valor Cero los que no tengan correspondencia y si el JSON fuese el que tiene mas, se descartar谩n.
Un ejemplo de un tipo simple, que representa solo los valores que nos interesan: `
// ResultadosML contiene un listado de resultados, representa una p谩gina de resultados.
type ResultadosML struct {
Results []ResultadoML `json:"results"`
}
// ResultadoML contiene el precio de un resultado, representa un item de una p谩gina de resultados
// pero no es para nada exaustivo.
type ResultadoML struct {
Price float64 `json:"price"`
}
// GetPrice devuelve el precio de un resultado convertido a decimal.Decimal.
func (r ResultadoML) GetPrice() decimal.Decimal {
return decimal.NewFromFloat(r.Price)
}Que intenta representar
{
"results": [
{"price":1900 },
{"price":1800 }
]
}Este JSON es un subconjunto de un resultado real, en el codigo de ejemplo hay un archivo JSON que contiene en realidad todo lo que hay en un resultado de una b煤squeda.
Re-pensado para ser serializado en estos tipos, podemos ver que entonces el c贸digo es mas legible y un poco mas declarativo, no hay necesidad de realizar aserciones para todo lo que hay entre la raiz del arbol y la hoja que nos interesa.
// en lugar de map[string]interface{} definimos el tipo concreto
resultML := &ResultadosML{}
// se de-serializa a un puntero de ResultadosML (siempre puntero en este caso)
err = json.Unmarshal(bodyData, resultML)
if err != nil {
return decimal.Zero, fmt.Errorf("unmarshaling mercado libre response body: %v", err)
}
// podemos chequear directamente el largo del slice Results porque sabemos que
// el chequeo de tipo se hizo durante la de-serializaci贸n
if len(resultML.Results) == 0 {
return decimal.Zero, fmt.Errorf("results not found in response")
}
result := resultML.Results[0]
// finalmente la conversi贸n a decimal.Decimal la realiza el mismo struct que sabe
// que su miembro es float, esto solo es posible por la estructura fija de los
// resultados
return result.GetPrice(), nilDe este modo podemos armar un main.go muy simple, en el ejemplo a continuaci贸n, para el que se provee un repositorio con todo el c贸digo para experimentaci贸n, contamos con la funcion dolarizame que esta en el archivo dolarizame.go que es una conversi贸n del ejercicio anterior a funci贸n y que dejaremos como ejercicio para quien su analisis.
func main() {
// moneyPrice, err := iPhoneMasCaroML(wg)
moneyPrice, err := iPhoneMasCaroMLStruct()
if err != nil {
log.Fatalf("no se puede obtener el costo del iphone de mercado libre: %v", err)
}
usd, err := dolarizame(moneyPrice)
if err != nil {
fmt.Printf("el iphone mas caro cuesta: AR$ %s\n", moneyPrice.StringFixedBank(2))
log.Fatalf("no se puede obtener la taza de cambio en dolares: %v", err)
}
fmt.Printf("el iphone mas caro cuesta: AR$ %s (U$D%s al promedio compra/venta)\n",
moneyPrice.StringFixedBank(2), usd.StringFixedBank(2))
}Si desean, pueden bajarse la carpeta de ejemplo y ejecutar:
go run main.go dolarizame.go
Obtendran un resultado como (lleva un rato dependiendo de su internet, paciencia):
el iphone mas caro cuesta: AR$ 197176.00 (U$D3414.30 al promedio compra/venta)
Este resultado no es para nada cient铆fico, es mas que nada did谩ctico para que experimenten consumir una API JSON y obtener datos de dos fuentes distintas si ojean dolarizame.go tambien pueden experimentar cambiando la velocidad o imprimiendo algunos otros resultados que no sean el mas caro o incluso cambiando el criterio de b煤squeda. Si茅ntanse libres de comentar con los resultados y preguntas.