---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Riduzione del ritardo di risposta nell''upload di file HTTP in Go: implementazione di una coda di worker asincroni basata su canali'
description: Questa struttura separa, nel gestore di caricamento file di net/http, la ricezione dei file temporanei dall'operazione di trasferimento verso il container Incus, utilizzando un canale con buffer e un pool di worker basati su goroutine, al fine di ridurre la latenza di risposta dell'endpoint.
language: it
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Panoramica

Quando si impara Go, capita spesso di farlo implementando un server backend. In questo caso, facciamo come esempio la creazione di un programma che riceve uno stream di file tramite una RestAPI o simili e lo carica sul server.
Il server net/http del linguaggio Go gestisce per impostazione predefinita più richieste contemporaneamente, quindi di per sé il caricamento simultaneo e concorrente non presenta problemi.
Tuttavia, se tutte le operazioni successive alla ricezione dello stream vengono eseguite in modo sincrono, la risposta dell'endpoint viene ritardata.
Vediamo una tecnica per prevenire questa situazione.

# Causa

Anche la ricezione di uno stream richiede generalmente molto tempo e, nel caso di file di grandi dimensioni, una singola richiesta può richiedere diversi minuti per essere elaborata.
In questi casi è importante elaborare il più rapidamente possibile le operazioni successive alla ricezione.
Lo scenario di questo esempio è quello di ricevere uno stream, salvarlo come file temporaneo e inviarlo a un container.
A questo punto, se la parte che invia il file temporaneo al container viene gestita tramite un worker pool, è possibile ridurre il ritardo della risposta.

```go
package file_upload

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"
)

const uploadTempDir = "/tmp/incus_uploads" // Directory temporanea sull'host

// UploadTask contiene i dati per l'invio asincrono del file.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler elabora i caricamenti dei file. Salva in un file temporaneo, poi accoda per l'invio a Incus.
func UploadHandler(wr http.ResponseWriter, req *http.Request) {
	if req.Method != http.MethodPost {
		http.Error(wr, "POST method required.", http.StatusMethodNotAllowed)
		return
	}
	originalFilePath := req.Header.Get("X-File-Path")
    originalFilename := filepath.Base(req.Header.Get("X-Host-Path"))
	containerName := req.Header.Get("X-Container-Name")
	if originalFilePath == "" || containerName == "" {
		http.Error(wr, "Missing X-File-Path or X-Container-Name header.", http.StatusBadRequest)
		return
	}

	cleanContainerDestPath := filepath.Clean(originalFilePath)
	if !filepath.IsAbs(cleanContainerDestPath) {
		http.Error(wr, "File path must be absolute.", http.StatusBadRequest)
		return
	}

	// Crea un percorso univoco per il file temporaneo sull'host
	tempFileName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), filepath.Base(originalFilePath))
	hostTempFilePath := filepath.Join(uploadTempDir, tempFileName)

	if err := os.MkdirAll(uploadTempDir, 0755); err != nil {
		log.Printf("ERROR: Failed to create temp upload directory: %v", err)
		http.Error(wr, "Server error.", http.StatusInternalServerError)
		return
	}

	// Crea il file temporaneo e copia il corpo della richiesta in esso (sincrono)
	outFile, err := os.Create(hostTempFilePath)
	if err != nil {
		log.Printf("ERROR: Failed to create temporary file: %v", err)
		http.Error(wr, "Server error.", http.StatusInternalServerError)
		return
	}
	defer outFile.Close()

	bytesWritten, err := io.Copy(outFile, req.Body)
	if err != nil {
		outFile.Close()
		os.Remove(hostTempFilePath)
		log.Printf("ERROR: Failed to copy request body to temp file: %v", err)
		http.Error(wr, "File transfer failed.", http.StatusInternalServerError)
		return
	}
	log.Printf("Upload Info: Received %d bytes, saved to %s.", bytesWritten, hostTempFilePath)

	// Accoda l'attività per l'invio asincrono a Incus
	task := UploadTask{
		HostTempFilePath:         hostTempFilePath,
		ContainerName:            containerName,
        HostFilename :            originalFilename,
		ContainerDestinationPath: cleanContainerDestPath,
	}
	EnqueueTask(task) //THIS PART
	log.Printf("Upload Info: Task enqueued for %s to %s.", originalFilePath, containerName)

	// Invia immediatamente la risposta 202 Accepted
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Guardando la parte indicata con THIS PART, si sarà notato che qui l'attività viene inserita in una coda.

Vediamo ora come funziona la coda delle attività.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

// InitWorkQueue inizializza la coda delle attività in memoria.
func InitWorkQueue() {
	once.Do(func() {
		taskQueue = make(chan UploadTask, 100)
		log.Println("Upload Info: Work queue initialized.")
	})
}

// EnqueueTask aggiunge un UploadTask alla coda.
func EnqueueTask(task UploadTask) {
	if taskQueue == nil {
		log.Fatal("ERROR: Task queue not initialized.")
	}
	taskQueue <- task
	log.Printf("Upload Info: Queue: Task enqueued. Size: %d", len(taskQueue))
}

// DequeueTask recupera un UploadTask dalla coda, bloccandosi se è vuota.
func DequeueTask() UploadTask {
	if taskQueue == nil {
		log.Fatal("ERROR: Task queue not initialized.")
	}
	task := <-taskQueue
	log.Printf("Upload Info: Queue: Task dequeued. Size: %d", len(taskQueue))
	return task
}

// GetQueueLength restituisce la dimensione attuale della coda.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

La coda delle attività fornita come esempio è implementata in modo semplice. Questa coda ha una struttura elementare che estrae dal canale le attività accodate.

Di seguito è riportato il metodo worker per inviare il file al container dopo il caricamento.
Il metodo attuale utilizza un ciclo infinito per garantire una buona reattività e un'implementazione semplice, ma è possibile aggiungere algoritmi in base all'uso previsto.

```go
func StartFilePushWorker() {
	for {
		task := DequeueTask()
		log.Printf("Upload Info: Worker processing task for %s from %s.", task.ContainerName, task.HostFilename)

		// Pianifica la pulizia del file temporaneo
		defer func(filePath string) {
			if err := os.Remove(filePath); err != nil {
				log.Printf("ERROR: Worker: Failed to remove temp file '%s': %v", filePath, err)
			} else {
				log.Printf("Upload Info: Worker: Cleaned up temp file: %s", filePath)
			}
		}(task.HostTempFilePath)

		// Elabora l'attività con nuovi tentativi per gli errori transitori di Incus
		for i := 0; i <= MaxRetries; i++ {
			err := processUploadTask(task) //separate upload task
			if err == nil {
				log.Printf("SUCCESS: Worker: Task completed for %s.", task.ContainerName)
				break
			}

			isTransient := true
			if err.Error() == "incus: container not found" { // Esempio di errore permanente
				isTransient = false
			}

			if isTransient && i < MaxRetries {
				log.Printf("WARNING: Worker: Task failed for %s (attempt %d/%d): %v. Retrying.",
					task.ContainerName, i+1, MaxRetries, err)
				time.Sleep(RetryDelay)
			} else {
				log.Printf("ERROR: Worker: Task permanently failed for %s after %d attempts: %v.",
					task.ContainerName, i+1, err)
				break
			}
		}
	}
}
```

Innanzitutto, questa funzione continua a girare in un ciclo cercando di prelevare le attività dalla coda. Successivamente, entro l'intervallo dei tentativi, tenta la fase di caricamento dal file temporaneo al container, non dallo stream al file temporaneo.

# Vantaggi

Il vantaggio di questo approccio è che, **purché il caricamento dello stream vada a buon fine**, è possibile ridurre il ritardo delle operazioni successive e prevenire l'esaurimento delle risorse causato da operazioni concorrenti sui container. Come si vede nel codice attuale, il numero di operazioni sui container che possono essere eseguite contemporaneamente è limitato dal numero dei canali. In questo modo abbiamo esaminato un esempio pratico di come utilizzare il parallelismo di Go. Per vedere più esempi, visitate i link seguenti.
[Modulo contenente esempi](https://github.com/gg582/linux_virt_unit)
[Progetto che utilizza gli esempi](https://github.com/gg582/incuspeed)
Il progetto stesso contiene molti componenti accessori, quindi per studiare il worker in sé è sufficiente vedere brevemente come viene richiamata la funzione init del worker da main.go. Il modulo include anche altri tipi di worker, quindi vi invitiamo a consultarli.
