Un cliente HTTP en Go es un programa que envía peticiones a otros servicios web y procesa sus respuestas.
Si en el artículo anterior montamos nuestro propio servidor, ahora nos ponemos en el otro lado. Vamos a consumir una API, leer JSON y gestionar los fallos normales de la vida real, que siempre aparecen cuando hay red de por medio.
Porque una cosa es llamar a http.Get en un ejemplo y otra muy distinta es escribir código que no se quede colgado eternamente si el servidor remoto decide tomarse la tarde libre.
La forma rápida: http.Get
Para una petición sencilla podemos usar http.Get.
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.example.com/status")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}No olvidéis cerrar resp.Body. Para que una conexión HTTP/1 pueda reutilizarse, normalmente debemos leer el cuerpo hasta el final y cerrarlo. En el ejemplo, io.ReadAll ya hace la primera parte.
Comprobar el código de estado
Que no haya error de red no significa que la respuesta sea correcta.
resp, err := http.Get("https://api.example.com/users")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("respuesta inesperada: %s", resp.Status)
}Un 404, un 500 o un 401 no son errores de transporte. Son respuestas HTTP válidas que tenemos que interpretar.
Decodificar JSON
Lo habitual es que una API nos devuelva JSON. Podemos decodificarlo directamente desde el cuerpo de la respuesta.
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
type Usuario struct {
ID int `json:"id"`
Nombre string `json:"nombre"`
Email string `json:"email"`
}
var clienteHTTP = &http.Client{Timeout: 5 * time.Second}
func obtenerUsuario(id int) (*Usuario, error) {
url := fmt.Sprintf("https://api.example.com/users/%d", id)
resp, err := clienteHTTP.Get(url)
if err != nil {
return nil, fmt.Errorf("consultar usuario %d: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("consultar usuario %d: respuesta %s", id, resp.Status)
}
var usuario Usuario
err = json.NewDecoder(resp.Body).Decode(&usuario)
if err != nil {
return nil, fmt.Errorf("decodificar usuario %d: %w", id, err)
}
return &usuario, nil
}Fijaos en que devolvemos (*Usuario, error). Es el patrón habitual en Go: resultado y error, sin esconder nada debajo de la alfombra.
Usar un cliente con timeout
http.Get usa el cliente por defecto, que no tiene un límite global de tiempo. Para código real, conviene crear y reutilizar nuestro propio http.Client.
cliente := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := cliente.Get("https://api.example.com/users")Ese límite incluye la conexión, las redirecciones y la lectura del cuerpo. Para controlar fases concretas podemos configurar el Transport o usar un contexto con fecha límite.
func nuevoCliente() *http.Client {
return &http.Client{
Timeout: 5 * time.Second,
}
}Los clientes y sus transportes son seguros para uso concurrente y deben reutilizarse para aprovechar el conjunto de conexiones.
Peticiones con headers
Cuando necesitamos cabeceras, tokens o métodos distintos, usamos http.NewRequestWithContext. El contexto permite cancelar la petición si el llamador termina o alcanza su fecha límite.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users", nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := cliente.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()cliente.Do(req) es la forma general. Permite usar cualquier método, añadir cabeceras y propagar cancelación.
Enviar JSON con POST
Para enviar datos, codificamos una estructura a JSON.
type CrearUsuario struct {
Nombre string `json:"nombre"`
Email string `json:"email"`
}
datos := CrearUsuario{
Nombre: "Ada",
Email: "[email protected]",
}
body, err := json.Marshal(datos)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://api.example.com/users",
bytes.NewReader(body),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := cliente.Do(req)
if err != nil {
return fmt.Errorf("crear usuario: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("crear usuario: respuesta %s", resp.Status)
}Aquí bytes.NewReader(body) convierte el []byte del JSON en algo que la petición puede leer como cuerpo.