json.Unmarshal vs NewDecoder.Decode: ¿cuál es más rápido en Go?

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Dos hermanos acróbatas: Unmarshal y Decode



Si programas en Go y trabajas con JSON, tarde o temprano te enfrentas a la elección: ¿usar json.Unmarshal o json.NewDecoder.Decode? La pregunta es tan popular que en Stack Overflow ha acumulado miles de visitas y decenas de votos. Pero la respuesta que encontrarás allí a menudo se reduce a "depende de la tarea".



Hoy no daremos una respuesta abstracta. Realizaremos nuestra propia investigación: analizaremos el funcionamiento interno de ambos métodos, escribiremos benchmarks, mostraremos ejemplos reales de código y estableceremos reglas claras sobre cuándo usar cada uno. ¡Abróchense los cinturones, que se pone intenso!



Resumen rápido: ¿qué son Unmarshal y Decode?



Ambas herramientas hacen lo mismo: convierten JSON en una estructura de Go. Pero lo hacen de manera diferente.



  • json.Unmarshal — recibe como entrada []byte y un puntero a una estructura. Todo de una vez: los bytes están listos, la estructura está preparada, ¡adelante! Funciona "de frente": deserializa todo el array de datos en memoria.
  • json.NewDecoder.Decode — trabaja con un flujo (io.Reader). Creas un decodificador, le pasas un reader (por ejemplo, os.File o http.Response.Body) y llamas a Decode. El decodificador lee los datos en fragmentos, sin cargar todo en memoria de una vez.


Parecería que la diferencia es obvia: para datos grandes, Decoder; para datos pequeños, Unmarshal. Pero el diablo, como siempre, está en los detalles.



El funcionamiento interno: cómo trabajan bajo el capó



json.Unmarshal — un disparo de precisión



Unmarshal es una función que:

  1. Recibe un slice de bytes.
  2. Asigna memoria para la estructura resultante.
  3. En una sola pasada analiza todo el JSON y llena la estructura.


Todo es simple y elegante. Pero hay un detalle: si pasas a Unmarshal un JSON grande (por ejemplo, 100 MB), primero debes leer esos 100 MB en memoria. Luego, Unmarshal asignará aún más memoria para la estructura. Resultado: consumo doble de RAM.



json.NewDecoder.Decode — el fontanero



Decoder funciona de otra manera:

  1. Se crea un nuevo decodificador que envuelve un io.Reader.
  2. El decodificador lee los datos en búferes (por defecto, 4 KB).
  3. A medida que lee, analiza el JSON token por token y llena la estructura.


La ventaja es que no almacenas todo el JSON en memoria. La desventaja es un poco más de sobrecarga en la gestión del búfer y la lectura en flujo.



Experimento: los benchmarks no mienten



Verifiquemos la teoría en la práctica. Escribamos un benchmark simple para tres escenarios:

  • JSON pequeño (1 KB)
  • JSON mediano (100 KB)
  • JSON grande (10 MB)


package main

import ( "bytes" "encoding/json" "testing")

type Data struct { ID int `json:"id"` Name string `json:"name"` Items []int `json:"items"`}

func generateJSON(size int) []byte { data := Data{ ID: 1, Name: "test", Items: make([]int, size), } for i := 0; i < size; i++ { data.Items[i] = i } b, _ := json.Marshal(data) return b}

func BenchmarkUnmarshal(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { json.Unmarshal(jsonData, &result) }}

func BenchmarkDecoder(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { reader := bytes.NewReader(jsonData) json.NewDecoder(reader).Decode(&result) }}


Resultados (en mi portátil, Go 1.22):



Tamaño

Blogs

Book Recommendations