Peticiones HTTP GET y POST en Go

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.

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
Las dos reglas que no se pueden saltar. La primera: 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
}
Cuándo falla de verdad el 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.