Cuando trabajas con JSON en Go, la decisión entre json.Unmarshal y json.NewDecoder no es una cuestión de preferencia, sino de gestión de memoria y arquitectura de datos. json.Unmarshal requiere que ya tengas todo el payload cargado en un []byte en la memoria RAM; es una operación atómica que procesa un bloque completo de datos. Por el contrario, json.NewDecoder opera sobre un io.Reader, lo que le permite procesar datos de forma incremental (streaming) a medida que estos llegan a través de una conexión o se leen de un archivo.
Si ya tienes los datos en memoria (por ejemplo, un pequeño fragmento de configuración), json.Unmarshal es más directo y sencillo. Sin embargo, cuando procesas respuestas de APIs HTTP o archivos de gran tamaño, usar Unmarshal te obliga a leer todo el cuerpo de la respuesta con io.ReadAll antes de empezar a decodificar, lo cual es un desperdicio de memoria innecesario. En esos casos, pasar el resp.Body directamente al Decoder permite que el runtime procese el flujo de datos sin necesidad de duplicar todo el contenido en un slice de bytes gigante.
Si intentas procesar un archivo de varios gigabytes con json.Unmarshal, tu proceso probablemente morirá con un error de Out of Memory (OOM). Además, si trabajas con números que requieren una precisión extrema (como IDs de transacciones muy largos o valores científicos), el decodificador por defecto de Go intentará convertirlos a float64, lo que puede causar pérdida de precisión; para evitar esto, el decodificador ofrece mecanismos para mantener el valor original como una cadena o un tipo especial.
package main
import (
"encoding/json"
"fmt"
"strings"
)
// SensorData representa la estructura que esperamos recibir.
type SensorData struct {
ID string `json:"id"`
Value json.Number `json:"value"` // Usamos json.Number para preservar la precisión
}
func main() {
// Simulamos un stream de datos que llega por la red.
// Nota que hay múltiples objetos JSON consecutivos, no están en un array.
rawStream := `{"id": "sensor-01", "value": 12345678901234567890}
{"id": "sensor-02", "value": 98.6}
{"id": "sensor-03", "value": "error_value"}`
reader := strings.NewReader(rawStream)
decoder := json.NewDecoder(reader)
// 1. Configuración crítica para validación estricta.
// Si el JSON trae campos que no definimos en la struct, fallará.
decoder.DisallowUnknownFields()
// 2. Configuración para precisión numérica.
// Evita la conversión automática a float64 que pierde precisión en enteros grandes.
decoder.UseNumber()
fmt.Println("Iniciando decodificación de stream...")
// 3. El bucle para streaming.
// decoder.More() verifica si hay más elementos en el stream de objetos.
for decoder.More() {
var data SensorData
if err := decoder.Decode(&data); err != nil {
// Si un objeto viene mal formado o con campos extra, lo capturamos aquí.
fmt.Printf("Error al decodificar objeto: %v\n", err)
break
}
// Como usamos UseNumber, Value.Int64() o Value.Float64() es seguro.
fmt.Printf("Decodificado -> ID: %s, Valor: %s\n", data.ID, data.Value)
}
fmt.Println("Proceso finalizado.")
}
Análisis del ejemplo
En el código anterior, hemos simulado un flujo de datos continuo utilizando strings.NewReader, que implementa la interfaz io.Reader.
Para garantizar la integridad de nuestros datos, hemos utilizado decoder.DisallowUnknownFields(). Esto es vital en sistemas de microservicios donde quieres que tu API falle explícitamente si un cliente envía campos que tu modelo no reconoce, evitando que proceses datos “sucios” o mal estructurados por error.
Un punto clave es el uso de json.Number en el campo Value de la struct SensorData, combinado con decoder.UseNumber(). En Go, por defecto, el paquete json convierte cualquier número en un float64. Para valores extremadamente grandes (como el de sensor-01 en el ejemplo), esto alteraría el valor original. UseNumber le dice al decoder que mantenga el número como una cadena interna, permitiéndonos decidir después si lo tratamos como entero o flotante sin perder precisión.
Finalmente, implementamos un bucle con decoder.More(). Esto es lo que realmente hace que el proceso sea eficiente para streaming: el decoder no espera a que el stream termine; procesa cada objeto JSON, lo entrega, y queda listo para el siguiente, manteniendo una huella de memoria mínima independientemente de si el stream contiene 3 o 3 millones de objetos.
El error frecuente
Un error muy común en servicios de alta carga es este patrón:
// MAL: Ineficiente y peligroso para payloads grandes
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var data MyStruct
if err := json.Unmarshal(body, &data); err != nil {
return err
}
Al hacer io.ReadAll, estás forzando a la máquina a asignar un bloque de memoria del tamaño exacto del cuerpo de la respuesta. Si recibes un JSON de 100MB, has consumido 100MB de RAM solo para leerlo, y probablemente otros 100MB mientras el decodificador procesa el objeto. En un servicio con miles de peticiones concurrentes, esto disparará el uso de memoria y provocará reinicios por el Garbage Collector o por falta de memoria del sistema. La solución siempre es json.NewDecoder(resp.Body).Decode(&data).
N° 121