Dans l’article précédent, MultipartReader garde la mémoire sous 2 Mio pour un upload de 1 Gio. Reste une question : comment ça marche réellement sous le capot ?
La réponse tient en une phrase :
NextPart()ne lit que les en-têtes, et c’estio.Copyqui tire les octets du fichier, un morceau à la fois.
📦 Ce que contient le corps de la requête
Le client envoie un seul flux d’octets.
Le fichier est au milieu, encadré par des marqueurs.
🪟 NextPart renvoie une fenêtre, pas des données
NextPart() consomme uniquement la zone bleue : le séparateur et les en-têtes, jusqu’à la ligne vide. Le Part renvoyé ne contient aucun octet du fichier. C’est un io.Reader posé sur le début des données, qui s’arrête tout seul au prochain séparateur.
part, err := reader.NextPart() // en-têtes lus, fichier pas encore touché
if err != nil {
return err
}
_, err = io.Copy(dst, part) // les octets du fichier passent ici
🔁 Qui tire sur quoi
Rien n’est lu tant que io.Copy ne demande rien : c’est un mécanisme de tirage (pull). Chaque tour de boucle fait la même chose :
io.Copyappellepart.Read(p)avec un tampon de 32 Ko ;- le
Partdemande des octets à sonbufio.Readerinterne, qui lit le socket si son tampon est vide ; - les octets remontent,
io.Copyles écrit dans le fichier, puis retour en 1.
C’est cette boucle qui explique les 1,8 Mio de pic mémoire.
🔚 Comment le Part sait que le fichier est fini ❓
Le Part ne connaît pas la taille du fichier : il avance en cherchant le séparateur --BOUNDARY. À chaque Read, trois cas :
- Séparateur absent du tampon : il rend les octets lus, mais garde en réserve les derniers. Le séparateur peut arriver coupé en deux entre deux lectures (
…--BOUpuisNDARY), et ces octets pourraient en être le début. - Séparateur trouvé : il rend les octets qui le précèdent. Au
Readsuivant, il répondio.EOF: le fichier est fini. - Tampon vide : il attend que le socket livre la suite, puis recommence.
🔂 Et la boucle sur NextPart ?
Une boucle for sur NextPart() existe, mais elle porte sur les parts, pas sur les octets : c’est le cas d’un formulaire avec plusieurs champs.
for {
part, err := reader.NextPart()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
if part.FileName() != "" {
return store(part)
}
}
⚠️ Pièges
- Les parts se lisent dans l’ordre : un second
NextPart()saute ce qui reste du premier sans le lire, ces octets sont perdus. - Un champ texte placé après le fichier n’est lisible qu’une fois le fichier entièrement consommé.
- Le streaming s’arrête où vous le coupez : stocker la part dans un
[]byteavant de l’écrire ramène le problème duReadAll.
