---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Redução do tempo de resposta em uploads de arquivos HTTP em Go: implementação de uma fila de workers assíncronos baseada em canais'
description: Trata-se de uma estrutura que, no manipulador de upload de arquivos do net/http, separa a recepção dos arquivos temporários e a operação de transferência para contêineres Incus em um canal com buffer e um pool de workers baseado em goroutines, com o objetivo de reduzir a latência de resposta do endpoint.
language: pt
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Visão geral

Ao aprender Go pela primeira vez, é comum aprendê-la implementando um servidor backend. Nesse caso, vamos considerar como exemplo a criação de um exemplo em que um arquivo é recebido como stream por meio de uma RestAPI e enviado ao servidor.
O servidor net/http da linguagem Go processa, por padrão, várias requisições simultaneamente, portanto não há problema no próprio upload simultâneo e em grande quantidade.
No entanto, se todas as ações após o recebimento do stream forem processadas de forma síncrona, a resposta do endpoint será atrasada.
Vamos conhecer uma técnica para evitar essa situação.

# Causa

Receber o stream geralmente também leva bastante tempo e, no caso de arquivos grandes, uma única requisição pode levar vários minutos para ser processada.
Nesses casos, é importante processar as ações posteriores ao recebimento o mais rápido possível.
O cenário deste exemplo é o de receber o stream, salvá-lo em um arquivo temporário e enviá-lo para um contêiner.
Nesse caso, se a parte que envia o arquivo temporário ao contêiner for tratada por um pool de workers, é possível reduzir o atraso da resposta.

```go
package file_upload

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

const uploadTempDir = "/tmp/incus_uploads" // Diretório temporário do host

// UploadTask mantém os dados para o envio assíncrono do arquivo.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler processa uploads de arquivos. Salva em arquivo temporário e, em seguida, enfileira para o envio ao 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
	}

	// Cria um caminho de arquivo temporário único no 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
	}

	// Cria o arquivo temporário e copia o corpo da requisição para ele (síncrono)
	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)

	// Enfileira a tarefa para o envio assíncrono ao 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)

	// Envia imediatamente a resposta 202 Accepted
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Na parte marcada como THIS PART, você deve ter percebido que a tarefa é inserida em uma fila.

Agora, vejamos como a fila de tarefas funciona.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

// InitWorkQueue inicializa a fila de tarefas em memória.
func InitWorkQueue() {
	once.Do(func() {
		taskQueue = make(chan UploadTask, 100)
		log.Println("Upload Info: Work queue initialized.")
	})
}

// EnqueueTask adiciona uma UploadTask à fila.
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 uma UploadTask da fila, bloqueando se estiver vazia.
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 retorna o tamanho atual da fila.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

A fila de tarefas fornecida como exemplo é implementada de forma simples. Essa fila possui uma estrutura básica que retira as tarefas enfileiradas do canal.

A seguir está o método worker responsável por enviar o arquivo ao contêiner após o upload.
Para boa responsividade e implementação simples, o método atual utiliza um loop infinito, mas nada impede que sejam adicionados algoritmos conforme a finalidade.

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

		// Agenda a limpeza do arquivo temporário
		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)

		// Processa a tarefa com novas tentativas para erros transitórios do 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" { // Exemplo de erro 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
			}
		}
	}
}
```

Primeiramente, esta função continua executando em loop, tentando obter as tarefas da fila. Em seguida, dentro do limite de novas tentativas, ela executa a etapa de upload do arquivo temporário para o contêiner, e não do stream para o arquivo temporário.

# Benefícios

A vantagem desse modo de processamento é que, **desde que o upload do stream esteja normal**, é possível reduzir o atraso das operações processadas posteriormente, além de prevenir o esgotamento de recursos causado por operações simultâneas e em grande quantidade nos contêineres. Como se vê no código atual, a quantidade de operações de contêiner que podem ocorrer simultaneamente é limitada pelo tamanho do canal. Dessa forma, apresentamos um exemplo prático que utiliza o paralelismo do Go. Para ver mais exemplos, acesse os links abaixo.
[Módulo com exemplos](https://github.com/gg582/linux_virt_unit)
[Projeto que utiliza os exemplos](https://github.com/gg582/incuspeed)
O projeto em si possui muitos componentes auxiliares, então, para estudar o próprio worker, basta observar brevemente como a função init do worker é chamada no main.go e seguir adiante. O módulo também contém outros tipos de worker, então consulte-os como referência.
