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:
- Importamos a Resty
- Declaramos nuestra URL base que consultaremos para traernos información
- 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
- Luego imprimimos toda la información recogida de nuestro request
Respuesta
Aquí está respuesta que nos regresa la llamada:
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:
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:
- Declaramos nuestro
struct
llamadoPost
que contiene los campos que esperamos de nuestro servicio. - Declaramos el tipo
Posts
que es unslice
dePost
. Aquí guardaremos la respuesta cuando no esperamos solo un registro, sino dos o más. - Dentro de
main
creamos nuestro cliente deresty
, creamos una estructura vacía de tipoPost
y luego apuntamos nuestro request hacia/posts/1
. - Validamos si hubo un error en la llamada
- Procesamos la respuesta
resp
para luego pasarlo a unstruct
mediantejson.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 variableerr
será diferente anil
, pero si buscamos un registro que no existe, el servidor nos regresará un código de error dentro de la respuesta, que podemos consultar enresp.StatusCode()
que posiblemente sea404
, 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)
}
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:
- Usamos el “path” de los archivos que enviaremos
- En
SetFileReader
enviamos el nombre del parámetro que espera el servicio, el nombre del archivo y elpath
. - 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.