Usar Resty en Golang

He estado trabajando en un servicio para un cliente que necesita integrar algunos otros servicios web que usa y conectarnos con sus respectivas API Rest. El proyecto está en Go y me parece buen ejercicio dejar este post sobre como usar Resty en Golang.

Para algunos ejemplos vamos a usar la API Rest de Placeholder.

Resty es una librería que crear un cliente para enviar y recibir información vía API y con todas las comodidades para tratar con esa información. Acá veremos cómo consultar una api con Golang con Resty.

Creando el proyecto

Empecemos por crearnos un proyecto en Go y lo primero que haremos es crear nuestro directorio:

mkdir resty
cd resty
go mod init resty

Luego nos instalamos el módulo:

go get github.com/go-resty/resty/v2

O lo puedes hacer desde los módulos, agregando la siguiente línea a go.mod dentro de nuestro directorio.

# Agregar esta línea en go.mod
require github.com/go-resty/resty/v2 v2.11.0

# Ejecutar esto para descargar y agregar dependencias
go mod tidy

Luego nos vamos a crear nuestro archivo principal

touch main.go

Primer ejemplo con Resty

En el main.go escribiremos lo siguiente:

package main

import (
    "fmt"

    "github.com/go-resty/resty/v2"
)

var (
    url = "https://jsonplaceholder.typicode.com"
)

func main() {
    client := resty.New()

    resp, err := client.R().
        EnableTrace().
        SetHeader("Content-Type", "application/json").
        Get(url + "/posts/1")

    fmt.Println("Response Info:")
    fmt.Println("  Error      :", err)
    fmt.Println("  Status Code:", resp.StatusCode())
    fmt.Println("  Status     :", resp.Status())
    fmt.Println("  Proto      :", resp.Proto())
    fmt.Println("  Time       :", resp.Time())
    fmt.Println("  Received At:", resp.ReceivedAt())
    fmt.Println("  Body       :\n", resp)
    fmt.Println()
}

En este ejemplo pasa lo siguiente:

  1. Importamos a Resty
  2. Declaramos nuestra URL base que consultaremos para traernos información
  3. Dentro de la función main creamos el cliente y hacemos el request a la URL. Aquí también establecemos que el contenido será application/json
  4. Luego imprimimos toda la información recogida de nuestro request

Respuesta

Aquí está respuesta que nos regresa la llamada:

usar resty en golang

Veremos el error en caso de que haya surgido, el código HTTP que nos regresa, el Status, Protocolo, tiempo del proceso y lo más importante, el Body. Éste es la respuesta tal cuál el servidor nos la envía y podrá ser procesada en diferentes escenarios que ya veremos.

EnableTrace

En nuestro ejemplo, vemos que está la siguiente línea en la preparación de nuestro request:

...
resp, err := client.R().
    EnableTrace().
...

Esta nos deja ver algunos elementos que en ciertos casos nos pueden ayudar. La respuesta es esta:

enabletrace en golang

Vemos que en su mayoría son los tiempos que se registran en ciertas acciones, como el tiempo de resolución de la URL, el tiempo de conexión o el handshake con el servicio. Además vemos que tenemos los intentos y la dirección IP y el puerto de la URl que estamos consultando.

El principio necesitaremos imprimir la respuesta completa del primer ejemplo y el trace para empezar a hacer debug.

Imprimir un struct (pretty print)

Antes de ya entrar en materia (se hace largo esto ¿no?). Crearemos una función para imprimir un struct indentado:

Creamos un archivo llamado fn.go y mete este código:

package main

import (
    "encoding/json"
    "fmt"
)

func ImprimeBonito(v any) {
    prettyJSON, err := json.MarshalIndent(v, "", "    ")
    if err != nil {
        fmt.Println("Error al convertir a JSON:", err)
        return
    }
    fmt.Println(string(prettyJSON))
}

Recibir información en un struct (modo 1)

Bueno, ya hemos visto un ejemplo muy básico y flags que nos ayudarán a debuggear errores. Ahora entraremos con casos de uso bastante normales dentro de nuestro código.

El primer ejemplo es asignar la información que nos regresa el servicio de posts/1 del servicio placeholder a un struct con los mismos campos.

Reemplazaremos todo lo de main.go y escribimos lo siguiente:

package main

import (
    "encoding/json"
    "fmt"

    "github.com/go-resty/resty/v2"
)

var (
    url = "https://jsonplaceholder.typicode.com"
)

type (
    Post struct {
        ID     int64  `json:"id"`
        UserID int64  `json:"userId"`
        Title  string `json:"title"`
    }

    Posts []Post
)

func main() {
    client := resty.New()

    post := Post{}

    resp, err := client.R().
        EnableTrace().
        SetHeader("Content-Type", "application/json").
        Get(url + "/posts/1")

    if err != nil {
        fmt.Println(err)
        return
    }

    err = json.Unmarshal([]byte(resp.String()), &post)
    if err != nil {
        fmt.Println("Error al convertir el JSON:", err)
        return
    }

    ImprimeBonito(post)
}

Puntos clave:

  1. Declaramos nuestro struct llamado Post que contiene los campos que esperamos de nuestro servicio.
  2. Declaramos el tipo Posts que es un slice de Post. Aquí guardaremos la respuesta cuando no esperamos solo un registro, sino dos o más.
  3. Dentro de main creamos nuestro cliente de resty, creamos una estructura vacía de tipo Post y luego apuntamos nuestro request hacia /posts/1.
  4. Validamos si hubo un error en la llamada
  5. Procesamos la respuesta resp para luego pasarlo a un struct mediante json.Unmarshal y luego lo imprimimos.

OJO: El error que nos puede regresar client.R()... está basado en nuestro request, no en la respuesta del servidor. Por ejemplo, si escribes mal la URL o no tienes conexión a Internet la variable err será diferente a nil, pero si buscamos un registro que no existe, el servidor nos regresará un código de error dentro de la respuesta, que podemos consultar en resp.StatusCode() que posiblemente sea 404, sin embargo, en términos reales, nuestro request estuvo bien formado y el servidor respondió correctamente.

Recibir información en un struct (modo 2)

En el modo 1, vimos que convertimos nuestra respuesta resp mediante json.Unmarshall, pero por fortuna, una de las ventajas de usar Resty en Golang es que 1tiene un método que nos facilita la gestión de la respuesta y carga la información directamente a un struct.

Para ellos usamos el método SetResult de la configuración de la llamada:

package main

import (
    "fmt"

    "github.com/go-resty/resty/v2"
)

//...

func main() {
    //...

    _, err := client.R().
        EnableTrace().
        SetHeader("Content-Type", "application/json").
        SetResult(&post).
        Get(url + "/posts/1")

    //...

    ImprimeBonito(post)
}

Vemos que estamos usando una referencia a post en la línea:

SetResult(&post)

Lo pasamos como referencia para que se “afecte” por la función y poder usarla luego con la información dentro.

Puedes probar que este modo y el anterior funcionan exactamente igual.

Recibir varios registros a un slice

En los ejemplos anteriores vimos como recoger la información y pasarla a un struct, sin embargo también es muy común recibir más de un registro, como resultado de una búsqueda, por ejemplo y para ello necesitamos un slice.

Omitiremos código repetido y modificaremos nuestro main.go:

// ... 
func main() {
    client := resty.New()

    var posts Posts

    _, err := client.R().
        EnableTrace().
        SetHeader("Content-Type", "application/json").
        SetResult(&posts).
        Get(url + "/posts")

    if err != nil {
        fmt.Println(err)
        return
    }

    for _, p := range posts {
        ImprimeBonito(p)
    }
}

Aquí declaramos la variable posts de nuestro tipo Posts ya declarado anteriormente como un slice.

Luego la enviamos a SetResult(&posts) y cambiamos la URL, que ya no será /post/1 sino /posts. En una API lo normal es que este tipo de enpoints tengan filtros por fechas, palabras o paginaciones.

Para finalizar hacemos un for-range para leer cada uno de los registros que nos regresó nuestro servicio.

Enviar información

Otra acción común es enviar información al servidor, ya sea para crear o actualizar registros. En este ejemplo usaremos POST para crear un post, pero también podemos conservar la estructura (solo cambiando el verbo) para las otras acciones.

package main

//...

type (
    Post struct {
        ID     int64  `json:"id"`
        UserID int64  `json:"userId"`
        Title  string `json:"title"`
        Body   string `json:"body"` // <--- agregamos este campo
    }

    Posts []Post
)

func main() {
    client := resty.New()

    post := Post{
        UserID: 1,
        Title:  "Mi post nuevo",
        Body:   "<b>Este es el body de un post que estoy enviando</b>",
    }

    _, err := client.R().
        EnableTrace().
        SetHeader("Content-Type", "application/json").
        SetResult(&post).
        Post(url + "/posts")

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%+v", post)
}
consultar una api en golang

En realidad, este servicio placeholder no crea nuestro post, pero nos regresa la información con el ID del post agregado.

Subir archivos

No siempre vamos a enviar solamente información en texto, por ejemplo JSON o XML, otras veces necesitamos enviar archivos junto a otra información y ahora veremos un ejemplo:

func main() {
    client := resty.New()

    archivoUno, _ := os.ReadFile("/ruta/al/archivo")
    archivoDos, _ := os.ReadFile("/ruta/al/archivo")

    _, err := client.R().
        EnableTrace().
        SetFileReader("archivo1", "ar1.png", bytes.NewReader(archivoUno)).
        SetFileReader("archivo2", "ar2.pdf", bytes.NewReader(archivoDos)).
        SetFormData(map[string]string{
            "otro_campo": "mi valor",
            "otro_valor": "mi valor",
        }).
        Put("https://miurl.com/endpoint-multipart")

    if err != nil {
        fmt.Println(err)
        return
    }
}

Puntos clave:

  1. Usamos el “path” de los archivos que enviaremos
  2. En SetFileReader enviamos el nombre del parámetro que espera el servicio, el nombre del archivo y el path.
  3. Con SetFormData enviamos información en texto plano.

SetAuth

Otra caso y con este cerramos el post, es que por lo general, nuestras API necesitan saber que hemos pasado por un proceso para validarnos como usuarios válidos en el sistema y que tenemos los permisos para poder enviar o leer información.

En este ejemplo, usaremos SetAuth para enviar un token al servicio.

func main() {
    client := resty.New()

    archivoUno, _ := os.ReadFile("/ruta/al/archivo")
    archivoDos, _ := os.ReadFile("/ruta/al/archivo")

    _, err := client.R().
        EnableTrace().
        SetAuthToken("MI_CLAVE_SECRETA_INNACESIBLE").
        SetFileReader("cer", "CSD.cer", bytes.NewReader(archivoUno)).
        SetFileReader("key", "CSD.key", bytes.NewReader(archivoDos)).
        SetFormData(map[string]string{
            "otro_campo": "mi valor",
            "otro_valor": "mi valor",
        }).
        Put("https://miurl.com/endpoint-multipart")

    if err != nil {
        fmt.Println(err)
        return
    }
}

Con SetAuthToken("MI_CLAVE_SECRETA_INNACESIBLE") le estamos indicando que será algo como Authentication: Bearer MI_CLAVE_SECRETA_INNACESIBLE.

Espero que este post sea de utilidad para que aprendas a usar Resty con Golang.

Gracias por leer.


Posted

in

, , , , ,

by