Deux frères acrobates : Unmarshal et Decode
\n\nSi vous codez en Go et travaillez avec du JSON, tôt ou tard vous serez confronté à un choix : utiliser json.Unmarshal ou json.NewDecoder.Decode ? La question est si populaire que sur Stack Overflow, elle a rassemblé des milliers de vues et des dizaines de votes. Mais la réponse que vous y trouverez se résume souvent à "cela dépend de la tâche".
Aujourd'hui, nous ne nous contenterons pas d'une réponse abstraite. Nous allons mener notre propre enquête : décortiquer les coulisses des deux méthodes, écrire des benchmarks, montrer des exemples de code concrets et établir des règles claires sur quand utiliser quoi. Attachez vos ceintures, ça va chauffer !
\n\nL'essentiel en bref : que sont Unmarshal et Decode ?
\n\nLes deux outils font la même chose : transformer du JSON en structure Go. Mais ils le font différemment.
\n\n- \n
json.Unmarshal— prend en entrée un[]byteet un pointeur vers une structure. Tout est immédiat : les octets sont là, la structure est prête, c'est parti. Il fonctionne "de front" — il désérialise l'intégralité des données en mémoire. \n json.NewDecoder.Decode— fonctionne avec un flux (io.Reader). Vous créez un décodeur, vous lui passez un reader (par exemple,os.Fileouhttp.Response.Body) et vous appelezDecode. Le décodeur lit les données par morceaux, sans tout charger en mémoire d'un coup. \n
La différence semble évidente : pour les grandes données, utilisez Decoder ; pour les petites, Unmarshal. Mais le diable se cache dans les détails, comme toujours.
\n\nDans les coulisses : comment ils fonctionnent sous le capot
\n\njson.Unmarshal — le tir de sniper
\n\nUnmarshal est une fonction qui :
\n- \n
- Prend une slice d'octets. \n
- Alloue de la mémoire pour la structure résultante. \n
- En un seul passage, parse tout le JSON et remplit la structure. \n
Tout est simple et élégant. Mais il y a un hic : si vous passez à Unmarshal un gros JSON (par exemple, 100 Mo), vous devez d'abord lire ces 100 Mo en mémoire. Ensuite, Unmarshal allouera encore de la mémoire pour la structure. Résultat : une double consommation de RAM.
\n\njson.NewDecoder.Decode — le plombier
\n\nDecoder fonctionne différemment :
\n- \n
- Un nouveau décodeur est créé, qui enveloppe un
io.Reader. \n - Le décodeur lit les données par tampons (par défaut 4 Ko). \n
- Au fur et à mesure de la lecture, il parse le JSON jeton par jeton et remplit la structure. \n
L'avantage : vous ne stockez pas tout le JSON en mémoire. L'inconvénient : un peu plus de frais généraux pour la gestion du tampon et la lecture en flux.
\n\nExpérience : les benchmarks ne mentent pas
\n\nVérifions la théorie en pratique. Écrivons un benchmark simple pour trois scénarios :
\n- \n
- Petit JSON (1 Ko) \n
- JSON moyen (100 Ko) \n
- Gros JSON (10 Mo) \n
package main\n\nimport (\n "bytes"\n "encoding/json"\n "testing"\n)\n\ntype Data struct {\n ID int `json:"id"`\n Name string `json:"name"`\n Items []int `json:"items"`\n}\n\nfunc generateJSON(size int) []byte {\n data := Data{\n ID: 1,\n Name: "test",\n Items: make([]int, size),\n }\n for i := 0; i < size; i++ {\n data.Items[i] = i\n }\n b, _ := json.Marshal(data)\n return b\n}\n\nfunc BenchmarkUnmarshal(b *testing.B) {\n jsonData := generateJSON(1000) // 1 Ko\n var result Data\n for i := 0; i < b.N; i++ {\n json.Unmarshal(jsonData, &result)\n }\n}\n\nfunc BenchmarkDecoder(b *testing.B) {\n jsonData := generateJSON(1000) // 1 Ko\n var result Data\n for i := 0; i < b.N; i++ {\n reader := bytes.NewReader(jsonData)\n json.NewDecoder(reader).Decode(&result)\n }\n}\n\nRésultats (sur mon ordinateur portable, Go 1.22) :
\n\n