El paquete net/http incluye también un cliente HTTP completo. El error más habitual es usar http.DefaultClient en producción: no tiene timeout, lo que puede dejar goroutines bloqueadas indefinidamente. Un cliente bien configurado con timeouts, transporte personalizado y contexto de cancelación es imprescindible en cualquier servicio real.
Get y Post básicos
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// GET simple (sin timeout solo para ejemplos)
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %dn%sn", resp.StatusCode, body)
}
http.Client con timeout
Define siempre un cliente propio con timeout. El timeout cubre todo el ciclo de vida de la petición: dial, handshake TLS, envío y lectura de la respuesta:
var clienteHTTP = &http.Client{
Timeout: 10 * time.Second,
}
func obtenerDatos(url string) ([]byte, error) {
resp, err := clienteHTTP.Get(url)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: status %d", url, resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
Petición con contexto y cancelación
http.NewRequestWithContext une el contexto con la petición. Cuando el contexto se cancela o expira, la petición se interrumpe limpiamente:
func buscarUsuario(ctx context.Context, id int) (*Usuario, error) {
url := fmt.Sprintf("https://api.example.com/usuarios/%d", id)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")
resp, err := clienteHTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("petición fallida: %w", err)
}
defer resp.Body.Close()
var u Usuario
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
return &u, nil
}
POST con body JSON
func crearUsuario(ctx context.Context, u NuevoUsuario) (*Usuario, error) {
datos, err := json.Marshal(u)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.example.com/usuarios", bytes.NewReader(datos))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := clienteHTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
cuerpo, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("error %d: %s", resp.StatusCode, cuerpo)
}
var resultado Usuario
json.NewDecoder(resp.Body).Decode(&resultado)
return &resultado, nil
}
Transport personalizado
http.Transport controla el pool de conexiones, los timeouts a nivel de TCP y TLS, y la compresión. En servicios con alta concurrencia conviene ajustarlo:
var clienteProduccion = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
},
}
Reintentos con backoff exponencial
func getConReintentos(ctx context.Context, url string, maxReintentos int) ([]byte, error) {
var ultimo error
for intento := 0; intento < maxReintentos; intento++ {
if intento > 0 {
espera := time.Duration(intento*intento) * 100 * time.Millisecond
select {
case <-time.After(espera):
case <-ctx.Done():
return nil, ctx.Err()
}
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := clienteHTTP.Do(req)
if err != nil {
ultimo = err
continue
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
ultimo = fmt.Errorf("status %d", resp.StatusCode)
continue
}
return io.ReadAll(resp.Body)
}
return nil, fmt.Errorf("tras %d reintentos: %w", maxReintentos, ultimo)
}
No reutilices http.DefaultTransport sin modificarlo: sus valores por defecto de MaxIdleConnsPerHost (2) son demasiado bajos para servicios con alta concurrencia, lo que provoca cuellos de botella en el pool de conexiones.
