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
maincreamos 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
structllamadoPostque contiene los campos que esperamos de nuestro servicio. - Declaramos el tipo
Postsque es unslicedePost. Aquí guardaremos la respuesta cuando no esperamos solo un registro, sino dos o más. - Dentro de
maincreamos nuestro cliente deresty, creamos una estructura vacía de tipoPosty luego apuntamos nuestro request hacia/posts/1. - Validamos si hubo un error en la llamada
- Procesamos la respuesta
resppara luego pasarlo a unstructmediantejson.Unmarshaly 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 variableerrserá 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
SetFileReaderenviamos el nombre del parámetro que espera el servicio, el nombre del archivo y elpath. - Con
SetFormDataenviamos 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.