Menu

Cliente HTTP en Golang: GET, POST JSON, timeouts y cabeceras

Cómo hacer peticiones HTTP en Go con net/http: http.Get, leer y cerrar el body, comprobar los códigos de estado, timeouts de http.Client, peticiones con context y cabeceras, parámetros de consulta y enviar JSON por POST.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una petición GET

Cada ejemplo de esta página arranca su propio servidor con httptest.NewServer y lo llama a través de una conexión TCP real en localhost. Cambia srv.URL por una dirección real como https://api.example.com y el código del cliente no cambia.

Las tres reglas que se ven aquí:

  1. Comprueba primero err. Informa de los fallos al obtener cualquier respuesta: errores de DNS, conexiones rechazadas, timeouts, problemas de TLS.
  2. defer resp.Body.Close() justo después de comprobar el error. El body mantiene abierta la conexión. Un body sin cerrar fuga la conexión.
  3. Lee o decodifica el body. io.ReadAll para texto, json.NewDecoder(resp.Body).Decode(&v) para JSON.

Los códigos de estado no son errores

http.Get devuelve err == nil para cualquier respuesta que envíe el servidor, incluidos 404 y 500. El estado lo tienes que comprobar tú:

io.LimitReader limita cuánto lees de un body de error, para que un servidor que se porta mal no te haga cargar megabytes en un mensaje de error. Muchas APIs devuelven 201, 202 o 204 como éxito; comprueba resp.StatusCode >= 200 && resp.StatusCode < 300 cuando te valga cualquier 2xx.

Timeouts: nunca uses el cliente por defecto para tráfico real

http.Get, http.Post y http.DefaultClient no tienen timeout global (el transporte por defecto solo limita la conexión y el handshake TLS). Un servidor que acepta la conexión y nunca responde bloquea tu goroutine para siempre. Crea tu propio cliente:

Client.Timeout cubre todo el intercambio: la conexión, las redirecciones y la lectura del body. El error que devuelve es un *url.Error que informa Timeout() == true, y errors.As lo encuentra a través de la interfaz net.Error. Crea un solo cliente y reutilízalo en todas partes; un http.Client es seguro para uso concurrente. El pool de conexiones vive en su Transport, no en el Client: un http.Client{} nuevo por petición sigue compartiendo http.DefaultTransport y su pool, pero un http.Transport nuevo por petición empieza cada vez con el pool vacío.

Peticiones con context y cabeceras

http.NewRequestWithContext construye una petición que puedes personalizar antes de enviarla con client.Do. El context da un deadline o una cancelación por petición, además del timeout del cliente, y ata las llamadas salientes a la vida de una petición entrante en un servidor.

Concatenar la entrada del usuario en una URL ("?q=" + q) se rompe con &, # y los espacios. url.Values y su método Encode hacen el escapado, y ordenan las claves. Para segmentos de la ruta, usa url.PathEscape.

POST con JSON

client.Post(url, contentType, body) es un atajo para una petición con esa cabecera Content-Type. Para cualquier otra cosa (una cabecera de autenticación, PUT, PATCH, DELETE) usa http.NewRequestWithContext con el método y client.Do. Para enviar formularios, client.PostForm(url, url.Values{...}) codifica los valores y pone application/x-www-form-urlencoded.

Reutilización de conexiones

El cliente reutiliza una conexión TCP solo cuando lees el body anterior hasta el final y lo cierras. Si dejas de leer antes (por ejemplo, tras un estado de error), vacíalo antes de cerrarlo cuando hagas muchas peticiones al mismo host:

defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()

Para bodies que podrían ser enormes, vacíalos a través de io.LimitReader, o acepta perder la conexión.

Redirecciones

El cliente sigue automáticamente hasta 10 redirecciones, y resp.Request.URL te dice dónde acabaste. Para detenerte en la primera redirección, por ejemplo para leer una cabecera Location, define CheckRedirect:

client := &http.Client{
	Timeout: 10 * time.Second,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse // return the 3xx response itself
	},
}

Errores comunes

  • Usar http.Get o http.DefaultClient en producción. Sin timeout. Crea un http.Client con Timeout.
  • Olvidar resp.Body.Close(). Las conexiones se fugan hasta que las peticiones empiezan a fallar.
  • Cerrar el body antes de comprobar err. Cuando err no es nil, resp es nil y resp.Body.Close() provoca un panic.
  • Tomar err == nil como éxito. Comprueba resp.StatusCode.
  • Crear un http.Transport nuevo por petición. Cada transporte tiene su propio pool de conexiones, así que cada petición abre una conexión nueva (y un handshake TLS nuevo). Comparte un solo transporte, normalmente a través de un único http.Client compartido.
  • Construir las query strings a mano. Usa url.Values.

Preguntas frecuentes

¿Cómo hago una petición GET en Go?

resp, err := http.Get(url), comprueba err, luego defer resp.Body.Close() y lee el body con io.ReadAll(resp.Body) o decodifícalo con json.NewDecoder(resp.Body).Decode(&v). Comprueba también resp.StatusCode: un 404 o un 500 no es un error de http.Get.

¿Cómo pongo un timeout a una petición HTTP en Go?

Crea un cliente con timeout, client := &http.Client{Timeout: 10 * time.Second}, y úsalo en lugar de http.Get o http.DefaultClient, que no tienen timeout global: un servidor que acepta la conexión y nunca responde bloquea la llamada para siempre. Para un límite por petición, construye la petición con http.NewRequestWithContext y un context de context.WithTimeout.

¿Cómo envío una petición POST con un body JSON en Go?

Serializa el valor y envíalo: body, _ := json.Marshal(v) y http.Post(url, "application/json", bytes.NewReader(body)). Para añadir cabeceras como Authorization, constrúyela con http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), asigna req.Header.Set("Content-Type", "application/json") y envíala con client.Do(req).

¿Por qué tengo que cerrar resp.Body en Go?

El body es un stream sobre una conexión abierta. Cerrarlo devuelve la conexión al pool para reutilizarla; olvidarlo fuga conexiones y descriptores de archivo hasta que el programa llega a su límite. Ciérralo aunque no lo leas, y léelo hasta el final si quieres que la conexión se reutilice.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR