---
id: 8wel7k12347894gsdjkfnru23kl78989
author: Yunjin Lee
title: 'Mempersingkat Waktu Respons Unggah File HTTP di Go: Implementasi Antrean Worker Asinkron Berbasis Channel'
description: Struktur ini memisahkan penerimaan file sementara dan pengiriman ke kontainer Incus dalam handler unggah file net/http menggunakan channel buffer dan kumpulan worker goroutine, sehingga latensi respons endpoint dapat dikurangi.
language: id
date: 2025-07-14T11:51:27.614782351Z
path: /blog/posts/go-handle-file-upload-with-concurrency-s844ce238
category: concurrency
---

# Ikhtisar

Ketika mempelajari Go untuk pertama kali, sering kali kita belajar sambil mengimplementasikan server backend. Mari kita ambil contoh membuat contoh yang menerima aliran file dari RestAPI atau sejenisnya dan mengunggahnya ke server.
Server net/http pada bahasa Go secara default memproses beberapa permintaan secara bersamaan, sehingga tidak ada masalah pada unggahan yang terjadi secara serentak dan bersamaan.
Namun, jika semua operasi setelah penerimaan aliran diproses secara sinkron, respons endpoint akan tertunda.
Mari kita pelajari teknik untuk mencegah situasi ini.

# Penyebab

Menerima aliran umumnya juga membutuhkan waktu yang cukup lama, dan untuk file berukuran besar, satu permintaan dapat diproses selama beberapa menit.
Dalam kasus seperti ini, penting untuk memproses operasi setelah penerimaan secepat mungkin.
Skenario contoh ini adalah skenario menyimpan aliran yang diterima sebagai file sementara, lalu mendorongnya ke sebuah container.
Pada saat ini, jika bagian yang mendorong file sementara ke container diproses dengan worker pool, keterlambatan respons dapat dipersingkat.

```go
package file_upload

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

const uploadTempDir = "/tmp/incus_uploads" // Direktori sementara host

// UploadTask menyimpan data untuk pendorongan file secara asinkron.
type UploadTask struct {
	HostTempFilePath         string
	ContainerName            string
    HostFilename             string
	ContainerDestinationPath string
}

// UploadHandler memproses unggahan file. Menyimpan ke file sementara, lalu memasukkannya ke antrean untuk pendorongan 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
	}

	// Membuat jalur file sementara yang unik di 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
	}

	// Membuat file sementara dan menyalin body permintaan ke dalamnya (sinkron)
	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)

	// Memasukkan tugas ke antrean untuk pendorongan Incus secara asinkron
	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)

	// Mengirim respons 202 Accepted secara langsung
	wr.WriteHeader(http.StatusAccepted)
	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
}
```

Jika Anda memperhatikan bagian yang diberi tanda THIS PART, Anda tentu menyadari bahwa tugas dimasukkan ke dalam antrean.

Sekarang mari kita lihat bagaimana antrean tugas bekerja.

```go
package file_upload

import (
	"log"
	"sync"
)

var taskQueue chan UploadTask
var once sync.Once

// InitWorkQueue menginisialisasi antrean tugas dalam memori.
func InitWorkQueue() {
	once.Do(func() {
		taskQueue = make(chan UploadTask, 100)
		log.Println("Upload Info: Work queue initialized.")
	})
}

// EnqueueTask menambahkan UploadTask ke antrean.
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 mengambil UploadTask dari antrean, memblokir jika kosong.
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 mengembalikan ukuran antrean saat ini.
func GetQueueLength() int {
	if taskQueue == nil {
		return 0
	}
	return len(taskQueue)
}
```

Antrean tugas yang disediakan sebagai contoh diimplementasikan secara sederhana. Antrean tugas ini memiliki struktur sederhana yang mengeluarkan tugas-tugas yang berada di dalam antrean dari channel.

Berikut ini adalah metode worker untuk mendorong file ke container setelah unggahan file.
Demi responsivitas yang baik dan kemudahan implementasi, metode saat ini menggunakan loop tak terbatas, namun algoritma dapat ditambahkan sesuai kebutuhan.

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

		// Menjadwalkan pembersihan file sementara dengan defer
		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)

		// Memproses tugas dengan percobaan ulang untuk kesalahan Incus yang bersifat sementara
		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" { // Contoh kesalahan permanen
				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
			}
		}
	}
}
```

Pertama, fungsi ini terus berputar dalam loop dan berusaha mengambil tugas dari antrean. Selanjutnya, dalam batas percobaan ulang, fungsi ini mencoba tahap unggahan dari file sementara ke container, bukan dari aliran ke file sementara.

# Manfaat

Manfaat dari cara pemrosesan ini adalah, **selama unggahan aliran berjalan normal**, waktu tunggu untuk operasi yang diproses setelahnya dapat dikurangi, dan kehabisan sumber daya akibat operasi container yang terjadi secara bersamaan dapat dicegah. Seperti terlihat pada kode saat ini, jumlah operasi container yang dapat berjalan secara bersamaan dibatasi oleh kapasitas channel. Dengan demikian, kami telah mempelajari contoh yang secara praktis dapat memanfaatkan pemrosesan paralel di Go. Jika Anda ingin melihat lebih banyak contoh, silakan kunjungi tautan di bawah ini.
[Modul yang berisi contoh](https://github.com/gg582/linux_virt_unit)
[Proyek yang memanfaatkan contoh](https://github.com/gg582/incuspeed)
Proyek itu sendiri memiliki banyak komponen pendukung, jadi untuk mempelajari worker itu sendiri, cukup lihat secara singkat bagaimana fungsi init worker dipanggil dari main.go lalu lanjutkan. Modul ini juga berisi worker dengan bentuk lain, silakan dijadikan referensi.
