Enviamos JSON en una solicitud POST en Go: de "cómo funciona esto" a "hecho y olvidado"
¿Alguna vez te has quedado atascado en Stack Overflow tratando de entender por qué tu solicitud POST con JSON en Go no es aceptada por el servidor? Te suena: parece que todo está correcto, pero la respuesta es 400 Bad Request o un cuerpo vacío. Resulta que en Go hay varias formas de enviar JSON, y elegir la correcta no es solo una cuestión de gusto, sino de la estabilidad de tu microservicio.
En este artículo no solo mostraremos código. Analizaremos tres enfoques: desde el ingenuo (que funciona pero es peligroso) hasta una solución de producción con manejo correcto de errores y contextos. Si escribes en Go para API, bots o integraciones, esta es una lectura obligatoria.
¿Por qué este tema es tan popular?
Go es un lenguaje para servicios de red. Tarde o temprano, cualquier desarrollador se enfrenta a la tarea: "enviar JSON en el cuerpo de una solicitud POST". Parece sencillo, pero hay matices:
- Es necesario serializar correctamente la estructura a JSON (marshaling).
- Es importante no olvidar el encabezado
Content-Type: application/json. - Es necesario cerrar correctamente el cuerpo de la respuesta.
- Se desea evitar fugas de memoria y pánicos.
Analicemos cada enfoque con ejemplos prácticos.
Enfoque 1: "Sobre la marcha" — bytes.NewBuffer + json.Marshal
Esta es la forma más simple. Tomamos una estructura, la convertimos a bytes, la colocamos en un buffer y la enviamos. Muchos desarrolladores principiantes de Go escriben exactamente así.
package main
import ( "bytes" "encoding/json" "fmt" "net/http")
type User struct { Name string `json:"name"` Email string `json:"email"`}
func main() { user := User{Name: "Iván Petrov", Email: "ivan@example.com"} // Paso 1: convertimos la estructura a JSON jsonData, err := json.Marshal(user) if err != nil { fmt.Println("Error de marshaling:", err) return } // Paso 2: creamos la solicitud POST resp, err := http.Post( "https://api.example.com/users", "application/json", bytes.NewBuffer(jsonData), ) if err != nil { fmt.Println("Error en la solicitud:", err) return } defer resp.Body.Close() fmt.Println("Estado:", resp.Status)}Ventajas y desventajas de este enfoque
- Ventaja: Mínimo código, comprensible para un principiante.
- Desventaja: Sin flexibilidad. No puedes agregar encabezados personalizados (autorización, trace-id). No hay control sobre los tiempos de espera. Si el servidor devuelve un error, no lo manejas.
- Desventaja: Problema potencial: si
json.Marshaldevuelve un error, lo capturas, pero en el código real a menudo se olvida verificarlo.
Este método sirve para prototipos y pruebas. Para producción, no.
Enfoque 2: "El correcto" — http.NewRequest + json.NewEncoder
La comunidad de Go recomienda usar http.NewRequest y json.NewEncoder. ¿Por qué? Porque da control total sobre la solicitud y es más eficiente en memoria.
package main
import ( "bytes" "encoding/json" "fmt" "net/http" "time")
type Order struct { ID int `json:"id"` Amount float64 `json:"amount"` Items []string `json:"items"`}
func sendOrder(order Order) error { // Creamos un buffer y un codificador var buf bytes.Buffer enc := json.NewEncoder(&buf) // Codificamos directamente en el buffer (sin []byte intermedio) if err := enc.Encode(order); err != nil { return fmt.Errorf("error de codificación JSON: %w", err) } // Creamos la solicitud req, err := http.NewRequest("POST", "https://api.example.com/orders", &buf) if err != nil { return fmt.Errorf("error al crear la solicitud: %w", err) } // Establecemos los encabezados req.Header.Set("Content-Type", "application/json") req.Hea