Construcción profesional de peticiones HTTP en Go

Para interactuar con servicios externos de forma robusta, no basta con llamar a http.Get. Necesitas construir *http.Request con control total sobre su ciclo de vida, metadatos y estructura de datos. Un http.Request es la representación de una intención de comunicación; contiene el método, la URL, los headers y, lo más importante, un cuerpo que debe satisfacer la interfaz io.Reader.

En producción, la regla de oro es usar http.NewRequestWithContext [disponible desde Go 1.13] en lugar de http.NewRequest. ¿Por qué? Porque en sistemas distribuidos, las redes fallan y los servidores remotos pueden quedar colgados. Si usas un request sin contexto, tu goroutine puede quedarse bloqueada esperando una respuesta que nunca llegará, provocando una fuga de recursos (goroutine leak) que eventualmente tumbará tu servicio. Al usar un context.Context con un timeout definido, garantizas que la operación se aborte si no se cumple el plazo.

Solo debes usar http.NewRequest si estás en un entorno de tests muy controlado o en scripts triviales donde el error de red no tiene consecuencias críticas. Si fallas en la gestión del contexto, tu servicio será incapaz de recuperarse de latencias espurias de terceros.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"
)

type UserPayload struct {
	ID    int    `json:"id"`
	Email string `json:"email"`
}

func main() {
	// 1. Definición de la URL base y parámetros
	baseURL := "https://httpbin.org/post"
	
	// 2. Preparación de datos y Body
	// El cuerpo debe ser un io.Reader. bytes.NewReader es ideal para []byte.
	payload := UserPayload{ID: 42, Email: "dev@example.com"}
	jsonBytes, err := json.Marshal(payload)
	if err != nil {
		panic(err) // En un ejemplo real, maneja el error adecuadamente
	}
	bodyReader := bytes.NewReader(jsonBytes)

	// 3. Gestión del ciclo de vida con Context
	// Creamos un contexto que cancela la petición tras 2 segundos.
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // Siempre liberar recursos del contexto

	// 4. Construcción del Request con Contexto
	// Esta es la forma idiomática para evitar goroutine leaks.
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bodyReader)
	if err != nil {
		panic(err)
	}

	// 5. Configuración de Headers
	// Set sobreescribe si el header ya existe; Add lo añade al final de la lista.
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Request-ID", "trace-12345")

	// 6. Query Parameters
	// Usar url.Values garantiza que el encoding de caracteres especiales sea correcto.
	params := url.Values{}
	params.Add("version", "v1")
	params.Add("debug", "true")
	req.URL.RawQuery = params.Encode()

	// 7. Ejecución de la petición
	// Usar un cliente con timeout propio es una doble capa de seguridad.
	client := &http.Client{
		Timeout: 5 * time.Second,
	}

	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("Error al ejecutar la petición: %v\n", err)
		return
	}
	// Es imperativo cerrar el Body para permitir la reutilización de la conexión TCP (Keep-Alive).
	defer resp.Body.Close()

	// 8. Procesamiento de la respuesta
	respData, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Error leyendo la respuesta: %v\n", err)
		return
	}

	fmt.Printf("Status: %s\n", resp.Status)
	fmt.Printf("Respuesta: %s\n", string(respData))
}

Desglose técnico

En el ejemplo, la clave de la robustez reside en http.NewRequestWithContext. Al pasarle el ctx con un timeout de 2 segundos, el http.Client monitoriza internamente la cancelación. Si el servidor de httpbin.org tardara 3 segundos, la función client.Do retornaría inmediatamente con un error de contexto, liberando la goroutine.

Para el cuerpo de la petición, hemos transformado un slice de bytes en un bytes.NewReader. Esto es necesario porque http.NewRequest espera una interfaz io.Reader, que es una abstracción que permite leer datos secuencialmente sin necesidad de cargar todo el contenido en una estructura compleja si estuviéramos usando un archivo o un stream.

Respecto a los parámetros de consulta, la asignación req.URL.RawQuery = params.Encode() es la forma segura de manejar la URL. Si intentas concatenar strings manualmente (ej. url + "?key=" + value), corres el riesgo de producir URLs inválidas si los valores contienen espacios, signos de interrogación o caracteres especiales. url.Values se encarga de ese escape de forma automática.

Finalmente, nota el defer resp.Body.Close(). En Go, el cliente HTTP reutiliza las conexiones TCP mediante Keep-Alive. Si no cierras el cuerpo de la respuesta, la conexión se queda en un estado “ocupado”, agotando los descriptores de archivos del sistema operativo y bloqueando el pool de conexiones.

El error frecuente

Un error clásico es intentar leer el cuerpo de la respuesta (resp.Body) dos veces. El objeto resp.Body es un stream (un lector), no un buffer en memoria. Una vez que ejecutas io.ReadAll(resp.Body), el puntero de lectura llega al final del stream. Si intentas leerlo de nuevo, obtendrás un cuerpo vacío o un error, sin importar lo que el servidor haya enviado originalmente. Si necesitas procesar los datos y luego volver a leerlos, debes leerlos una vez, guardarlos en un slice de bytes y usar bytes.NewReader(data) para crear un nuevo lector.

186

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio