---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Reduktion af svartid ved HTTP-filupload i Go: implementering af en asynkron worker-kø baseret på kanaler'
description: Denne struktur adskiller modtagelsen af midlertidige filer og overførslen til Incus-containere i en net/http-filoploadhåndterer ved hjælp af en bufferet kanal og en pulje af goroutine-arbejdere, hvilket reducerer responsforsinkelsen for endepunktet.
language: da
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Oversigt

Når man første gang lærer Go, sker det ofte, at man lærer ved at implementere en backend-server. Lad os her tage som eksempel et tilfælde, hvor man laver et eksempel, der modtager en filstrøm via en RestAPI o.lign. og uploader den til serveren.
Go-sprogets net/http-server håndterer som standard flere forespørgsler samtidigt, så der er i sig selv ingen problemer med samtidige, mange-til-én-uploads.
Men hvis alle handlinger efter modtagelsen af strømmen udføres synkront, forsinkes endpointets svar.
Lad os se på teknikker til at forhindre denne situation.

# Årsag

Det tager som regel også lang tid at modtage en strøm, og for store filer kan en enkelt forespørgsel tage flere minutter at behandle.
I et sådant tilfælde er det vigtigt at behandle handlingerne efter modtagelsen så hurtigt som muligt.
Scenariet i dette eksempel er at gemme strømmen som en midlertidig fil og derefter skubbe den til en container. Hvis delen, hvor den midlertidige fil skubbes til containeren, behandles med en worker pool, kan svartiden forkortes.

```go
package file_upload

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

const uploadTempDir = "/tmp/incus_uploads" // Midlertidig mappe på værten

// UploadTask holder data til asynkron filoverførsel.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler behandler filuploads. Gemmer til midlertidig fil og sætter derefter i kø til Incus-overførsel.
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
	}

	// Opret unik midlertidig filsti på værten
	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
	}

	// Opret midlertidig fil og kopier request body til den (synkront)
	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)

	// Sæt opgave i kø til asynkron Incus-overførsel
	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)

	// Send øjeblikkeligt 202 Accepted-svar
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Når man ser på den del, der er markeret med THIS PART, vil du have bemærket, at opgaven indsættes i en kø.

Lad os nu se, hvordan opgavekøen fungerer.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

// InitWorkQueue initialiserer den in-memory opgavekø.
func InitWorkQueue() {
	once.Do(func() {
		taskQueue = make(chan UploadTask, 100)
		log.Println("Upload Info: Work queue initialized.")
	})
}

// EnqueueTask tilføjer en UploadTask til køen.
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 henter en UploadTask fra køen og blokerer, hvis køen er tom.
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 returnerer den aktuelle køstørrelse.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

Den eksempelvis angivne opgavekø er implementeret på en enkel måde. Denne opgavekø har en simpel struktur, der tager opgaverne ud af kanalen i den rækkefølge, de er blevet sat i kø.

Nedenfor er worker-metoden til at skubbe filen til containeren efter filoverførslen.
Af hensyn til god responsivitet og nem implementering er den nuværende metode en uendelig løkke, men der kan tilføjes algoritmer afhængigt af formålet.

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

		// Udskyd oprydning af den midlertidige fil
		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)

		// Behandl opgaven med genforsøg ved midlertidige Incus-fejl
		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" { // Eksempel på en permanent fejl
				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
			}
		}
	}
}
```

Først kører denne funktion en løkke, der gentagne gange forsøger at hente opgaver fra køen. Derefter forsøger den, inden for rammerne af genforsøgene, at udføre uploadtrinnet fra den midlertidige fil til containeren, og ikke fra strømmen til den midlertidige fil.

# Fordele

Fordelen ved denne behandlingsmetode er, at så længe **strømuploadet fungerer korrekt**, kan forsinkelsen for de efterfølgende handlinger reduceres, og ressourceudtømning på grund af samtidige containeroperationer kan forebygges. Som det fremgår af den nuværende kode, er antallet af containeroperationer, der kan køre samtidigt, begrænset af kanalens kapacitet. Sådan har vi gennemgået et eksempel, der praktisk anvender Gos parallelle processering. Hvis du ønsker at se flere eksempler, så besøg linkene nedenfor.
[Modul med eksempler](https://github.com/gg582/linux_virt_unit)
[Projekt, der anvender eksemplet](https://github.com/gg582/incuspeed)
Projektet i sig selv indeholder mange sekundære komponenter, så for at lære om selve workeren er det tilstrækkeligt kort at se, hvordan worker-init-funktionen kaldes i main.go, og så gå videre. Modulet indeholder også andre former for workers, så se gerne dem også.
