El cliente HTTP viene en la biblioteca estándar, sin dependencias. Lo único que conviene saber antes de empezar son dos cosas que sorprenden: hay que cerrar el cuerpo de la respuesta siempre, y un 404 no es un error para Go.
También te puede interesar
Un GET
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
cliente := &http.Client{Timeout: 10 * time.Second}
r, err := cliente.Get("https://example.com")
if err != nil {
fmt.Println("error:", err)
return
}
defer r.Body.Close()
cuerpo, _ := io.ReadAll(r.Body)
fmt.Println("codigo:", r.StatusCode)
fmt.Println("tipo :", r.Header.Get("Content-Type"))
fmt.Println("bytes :", len(cuerpo))
}
salidacodigo: 200 tipo : text/html; charset=utf-8 bytes : 577
defer r.Body.Close() justo después de comprobar el error, siempre; si no se cierra, la conexión no se reutiliza y acabas quedándote sin descriptores. La segunda: usa tu propio http.Client con Timeout. El cliente por defecto, el de http.Get(...), no tiene tiempo límite y puede dejar tu programa colgado para siempre.Decodificar JSON de la respuesta
No hace falta leer el cuerpo a una cadena y luego parsearlo: el decodificador trabaja directamente sobre el flujo.
type Respuesta struct {
Origen string `json:"origin"`
URL string `json:"url"`
}
r, err := cliente.Get("https://httpbin.org/get")
if err != nil {
return err
}
defer r.Body.Close()
var datos Respuesta
if err := json.NewDecoder(r.Body).Decode(&datos); err != nil {
return fmt.Errorf("decodificando la respuesta: %w", err)
}
fmt.Println("url devuelta:", datos.URL)
salidaurl devuelta: https://httpbin.org/get
Las etiquetas `json:"origin"` de la struct son las que atan cada campo con su nombre en el JSON. Sin ellas, Go buscaría un campo llamado Origen.
Un POST
cuerpo := strings.NewReader(`{"nombre":"Ana","unidades":3}`)
r, err := cliente.Post("https://httpbin.org/post", "application/json", cuerpo)
if err != nil {
return err
}
defer r.Body.Close()
fmt.Println("codigo:", r.StatusCode)
salidacodigo: 200
El tercer argumento es cualquier cosa de la que se pueda leer. Para una cadena, strings.NewReader; para un archivo, el propio *os.File; para una struct, lo habitual es codificarla a un bytes.Buffer con json.NewEncoder.
Y aquí está la trampa: el 404
r, err := cliente.Get("https://example.com/no-existe")
fmt.Println("err vale:", err)
if err == nil {
defer r.Body.Close()
fmt.Println("codigo:", r.StatusCode)
}
salidaerr vale: <nil> codigo: 404
El err vale nil. Para Go la petición fue perfectamente: se conectó, mandó y recibió una respuesta. Que esa respuesta sea un 404 es información, no un fallo de red.
La consecuencia práctica es que hay que comprobar dos cosas en cada petición, siempre:
r, err := cliente.Get(url)
if err != nil {
return fmt.Errorf("pidiendo %s: %w", url, err) // no hubo respuesta
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return fmt.Errorf("pidiendo %s: respondió %s", url, r.Status) // hubo, pero mala
}
err. Cuando no hay respuesta: el dominio no resuelve, la conexión se rechaza, el certificado no vale, o se agotó el tiempo límite. Todo lo que el servidor conteste —400, 404, 500— llega por StatusCode.Lo esencial, en una tabla
| Para | Usa |
|---|---|
| Un GET rápido | cliente.Get(url) |
| Un POST con cuerpo | cliente.Post(url, tipo, lector) |
| Poner cabeceras, o usar PUT y DELETE | http.NewRequest y cliente.Do(req) |
| No quedarte colgado | &http.Client{Timeout: …} |
| Leer JSON de la respuesta | json.NewDecoder(r.Body).Decode(&v) |
| Cerrar | defer r.Body.Close(), sin excepción |
Todo el código se compiló y ejecutó con Go 1.25 antes de publicarla, contra example.com y httpbin.org; las salidas están copiadas de esa ejecución. Documentación oficial: paquete net/http.