---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Zkrácení doby odezvy při nahrávání souborů přes HTTP v Go: implementace asynchronní pracovní fronty založené na kanálech'
description: Tato struktura odděluje příjem dočasných souborů a přenos do kontejneru Incus v rámci handleru pro nahrávání souborů v net/http do bufferovaného kanálu a poolu worker gorutin, čímž snižuje latenci odezvy koncového bodu.
language: cs
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Přehled

Při prvním seznámení s jazykem Go se často stává, že se uživatelé učí jeho základy při implementaci backendových serverů. Jako příklad si uveďme situaci, kdy vytváříme ukázkovou aplikaci, která přijímá datový proud (stream) souborů prostřednictvím RestAPI a nahrává jej na server.
Server v jazyce Go postavený na balíčku net/http standardně zpracovává více požadavků současně, takže s paralelním nahráváním jako takovým není problém.
Pokud však veškeré operace následující po přijetí datového proudu zpracováváme synchronně, dochází ke zpoždění odpovědi koncového bodu (endpoint).
Seznamme se nyní s technikami, které slouží k prevenci takových situací.

# Příčina

Přijímání datového proudu obvykle vyžaduje delší dobu a v případě rozměrných souborů může zpracování jediného požadavku trvat i několik minut.
Za takových okolností je nanejvýš důležité provést operace následující po příjmu dat s maximální možnou rychlostí.
Tento vzorový scénář spočívá v přijetí datového proudu, jeho uložení do dočasného souboru a následném odeslání do kontejneru.
Pokud v tomto případě zajistíme odeslání dočasného souboru do kontejneru pomocí pracovní fronty (worker pool), můžeme zkrátit dobu prodlevy odpovědi.

```go
package file_upload

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

const uploadTempDir = "/tmp/incus_uploads" // Host temporary directory

// UploadTask holds data for asynchronous file push.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler processes file uploads. Saves to temp file, then queues for Incus push.
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
	}

	// Create unique temporary file path on 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
	}

	// Create and copy request body to temporary file (synchronous)
	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)

	// Enqueue task for asynchronous Incus push
	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 immediate 202 Accepted response
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Když se podíváme na část označenou jako THIS PART, jistě jste si všimli, že úkol se vkládá do fronty.

Nyní se podívejme, jakým způsobem pracovní fronta funguje.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

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

// EnqueueTask adds an UploadTask to the queue.
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 retrieves an UploadTask from the queue, blocking if empty.
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 returns current queue size.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

Pracovní fronta poskytnutá v příkladu je implementována jednoduše. Má prostou strukturu, která odebírá úkoly nacházející se ve frontě z příslušného kanálu.

Následující kód představuje metodu workeru určenou k odeslání souboru do kontejneru po jeho nahrání.
Tato metoda využívá nekonečnou smyčku z důvodu zajištění dobré odezvy a snadné implementace, avšak v závislosti na konkrétním účelu použití je možné do ní zakomponovat dodatečné algoritmy.

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

		// Defer cleanup of the temporary file
		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)

		// Process task with retries for transient Incus errors
		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" { // Example permanent error
				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
			}
		}
	}
}
```

Tato funkce nejprve v neustálé smyčce pokaždé usiluje o získání úkolu z fronty. Následně se v rámci povoleného počtu opakování pokusí provést fázi nahrávání nikoli z datového proudu do dočasného souboru, nýbrž z dočasného souboru do kontejneru.

# Výhody

Předností tohoto způsobu zpracování je skutečnost, že **za předpokladu úspěšného nahrání datového proudu** lze zkrátit dobu odezvy pro následně zpracovávané operace a předejít vyčerpání systémových zdrojů v důsledku souběžně probíhajících operací v kontejneru. Jak je z aktuálního kódu patrné, počet kontejnerových operací, které mohou probíhat současně, je omezen kapacitou kanálu. Tímto způsobem jsme si představili praktický příklad využití paralelního zpracování v jazyce Go. Máte-li zájem zhlédnout další ukázky, navštivte níže uvedené odkazy.
[Modul včetně příkladů](https://github.com/gg582/linux_virt_unit)
[Projekt využívající příklady](https://github.com/gg582/incuspeed)
Vzhledem k tomu, že samotný projekt obsahuje mnoho podpůrných součástí, prostudování samotného workeru lze provést jednoduše zběžným nahlédnutím do souboru main.go, kde je znázorněno, jakým způsobem se volá inicializační funkce workeru. Modul navíc obsahuje i jiné typy workerů, které mohou posloužit jako inspirace.
