---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Skrátenie oneskorenia odpovede pri nahrávaní súborov cez Go HTTP: implementácia asynchrónnej worker fronty založenej na kanáloch'
description: Štruktúra oddeľuje príjem dočasných súborov a prenos do kontajnerov Incus v obslužnej rutine nahrávania súborov v net/http pomocou bufferovaného kanála a fondu worker goroutín, čím sa znižuje oneskorenie odpovede koncového bodu.
language: sk
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Prehľad

Pri prvom učení jazyka Go sa často stáva, že sa jazyk učíme implementáciou backendového servera. Uveďme si príklad, v ktorom sa v RestAPI alebo podobnom rozhraní prijíma prúd súboru (file stream) a nahráva sa na server.
Server net/http v jazyku Go v predvolenom nastavení spracúva viacero požiadaviek súbežne, takže samotné súbežné nahrávanie viacerých súborov nepredstavuje problém.
Ak však všetky operácie po prijatí prúdu spracúvame synchrónne, odpoveď endpointu sa oneskorí.
Pozrime sa na techniku, ktorá tomuto stavu zabraňuje.

# Príčina

Prijatie prúdu zvyčajne trvá dlho a pri veľkých súboroch môže spracovanie jednej požiadavky trvať niekoľko minút.
V takomto prípade je dôležité spracovať operácie nasledujúce po prijatí čo najrýchlejšie.
Scenár tohto príkladu je: prijať prúd, uložiť ho ako dočasný súbor a odoslať ho do kontajnera.
Ak časť odosielania dočasného súboru do kontajnera spracujeme pomocou worker pool, môžeme skrátiť oneskorenie odpovede.

```go
package file_upload

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

const uploadTempDir = "/tmp/incus_uploads" // Dočasný adresár hostiteľa

// UploadTask uchováva dáta pre asynchrónne odoslanie súboru.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler spracúva nahrávanie súborov. Uloží ich do dočasného súboru a potom ich zaradí do fronty na odoslanie do 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
	}

	// Vytvorí jedinečnú cestu k dočasnému súboru na hostiteľovi
	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
	}

	// Vytvorí dočasný súbor a skopíruje doň telo požiadavky (synchrónne)
	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)

	// Zaradí úlohu do fronty na asynchrónne odoslanie do 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)

	// Okamžite odošle odpoveď 202 Accepted
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Ak sa pozriete na časť označenú THIS PART, zrejme ste si všimli, že sa tu úloha vkladá do fronty.

Pozrime sa teraz na to, ako pracuje fronta úloh.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

// InitWorkQueue inicializuje frontu úloh v pamäti.
func InitWorkQueue() {
	once.Do(func() {
		taskQueue = make(chan UploadTask, 100)
		log.Println("Upload Info: Work queue initialized.")
	})
}

// EnqueueTask pridá UploadTask do fronty.
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 získa UploadTask z fronty; ak je fronta prázdna, blokuje.
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 vráti aktuálnu veľkosť fronty.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

Fronta úloh uvedená ako príklad je implementovaná jednoducho. Má jednoduchú štruktúru, ktorá z channelu vyberá úlohy čakajúce vo fronte.

Nižšie je worker metóda, ktorá po nahratí súboru odosiela súbor do kontajnera.
Aktuálna metóda používa nekonečnú slučku, čo zabezpečuje dobrú odozvu a jednoduchú implementáciu; podľa účelu však možno pridať aj ďalšie algoritmy.

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

		// Odložené odstránenie dočasného súboru
		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)

		// Spracuje úlohu s opakovaniami pri dočasných chybách Incus
		for i := 0; i <= MaxRetries; i++ {
			err := processUploadTask(task) //samostatná úloha nahrávania
			if err == nil {
				log.Printf("SUCCESS: Worker: Task completed for %s.", task.ContainerName)
				break
			}

			isTransient := true
			if err.Error() == "incus: container not found" { // Príklad trvalej chyby
				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
			}
		}
	}
}
```

Najprv táto funkcia v nekonečnej slučke neustále sa pokúša prijímať úlohy z fronty. Potom v rámci rozsahu opakovaní vykoná krok nahrávania, ktorý nie je prenosom prúd-dočasný súbor, ale prenosom dočasný súbor-kontajner.

# Výhody

Výhodou tohto spôsobu spracovania je, že **ak je nahrávanie prúdu úspešné**, môžeme skrátiť oneskorenie operácií, ktoré nasledujú, a zabrániť vyčerpaniu zdrojov spôsobenému súbežnými operáciami s kontajnermi. Ako je vidieť v aktuálnom kóde, počet kontajnerových operácií, ktoré môžu prebiehať súčasne, je obmedzený počtom channelov. Takto sme si prakticky ukázali príklad, ako používať paralelné spracovanie v jazyku Go. Ak chcete vidieť viac príkladov, navštívte prosím odkazy nižšie.
[modul obsahujúci príklady](https://github.com/gg582/linux_virt_unit)
[projekt využívajúci príklady](https://github.com/gg582/incuspeed)
Projekt sám obsahuje veľa pomocných komponentov, preto pri štúdiu samotného workera stačí iba stručne pozrieť, ako sa init funkcia workera volá v main.go. Modul obsahuje aj iné typy workerov, pozrite si ich prosím.
