Works with any backend npm install Zero dependencies
← All Backend Guides

Go

Handle MultipleUpload file uploads with Go's standard library net/http package and multipart.Reader. No external dependencies required.

Multipart Form Upload

Use Go's built-in multipart form parsing. Files are saved to disk with a unique GUID filename.

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/google/uuid"
)

const uploadDir = "./uploads"
const chunkDir = "./uploads/chunks"
const maxUploadSize = 200 << 20 // 200 MB

func main() {
	os.MkdirAll(uploadDir, 0755)
	os.MkdirAll(chunkDir, 0755)

	mux := http.NewServeMux()
	mux.HandleFunc("/upload", corsMiddleware(handleUpload))
	mux.HandleFunc("/upload-chunk", corsMiddleware(handleChunkUpload))

	fmt.Println("Go upload server on http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers",
			"Content-Type, X-Upload-Id, X-Chunk-Index, X-Chunk-Count, X-File-Name, X-File-Size")

		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next(w, r)
	}
}

func handleUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
	if err := r.ParseMultipartForm(32 << 20); err != nil {
		jsonError(w, "File too large", http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		jsonError(w, "No file uploaded", http.StatusBadRequest)
		return
	}
	defer file.Close()

	fileGuid := uuid.New().String()
	ext := filepath.Ext(header.Filename)
	destPath := filepath.Join(uploadDir, fileGuid+ext)

	dest, err := os.Create(destPath)
	if err != nil {
		jsonError(w, "Failed to save file", http.StatusInternalServerError)
		return
	}
	defer dest.Close()

	io.Copy(dest, file)

	jsonResponse(w, map[string]interface{}{
		"success":  true,
		"fileGuid": fileGuid,
		"fileName": header.Filename,
		"fileSize": header.Size,
	})
}

Chunked Upload

Each chunk arrives as a raw body with custom headers. Store chunks in a temporary directory and assemble the final file once all chunks have arrived.

func handleChunkUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	uploadId := r.Header.Get("X-Upload-Id")
	chunkIndex, _ := strconv.Atoi(r.Header.Get("X-Chunk-Index"))
	chunkCount, _ := strconv.Atoi(r.Header.Get("X-Chunk-Count"))
	fileName := r.Header.Get("X-File-Name")
	fileSize, _ := strconv.ParseInt(r.Header.Get("X-File-Size"), 10, 64)

	if uploadId == "" || chunkCount == 0 {
		jsonError(w, "Missing chunk headers", http.StatusBadRequest)
		return
	}

	sessionDir := filepath.Join(chunkDir, uploadId)
	os.MkdirAll(sessionDir, 0755)

	// Save this chunk
	chunkPath := filepath.Join(sessionDir, fmt.Sprintf("chunk_%d", chunkIndex))
	chunkFile, err := os.Create(chunkPath)
	if err != nil {
		jsonError(w, "Failed to save chunk", http.StatusInternalServerError)
		return
	}
	io.Copy(chunkFile, r.Body)
	chunkFile.Close()

	// Count received chunks
	entries, _ := os.ReadDir(sessionDir)
	received := len(entries)

	if received == chunkCount {
		// Assemble final file
		fileGuid := uuid.New().String()
		ext := filepath.Ext(fileName)
		finalPath := filepath.Join(uploadDir, fileGuid+ext)

		output, err := os.Create(finalPath)
		if err != nil {
			jsonError(w, "Failed to assemble file", http.StatusInternalServerError)
			return
		}

		for i := 0; i < chunkCount; i++ {
			cp := filepath.Join(sessionDir, fmt.Sprintf("chunk_%d", i))
			chunk, _ := os.Open(cp)
			io.Copy(output, chunk)
			chunk.Close()
		}
		output.Close()

		// Clean up chunks
		os.RemoveAll(sessionDir)

		jsonResponse(w, map[string]interface{}{
			"success":  true,
			"fileGuid": fileGuid,
			"fileName": fileName,
			"fileSize": fileSize,
		})
		return
	}

	jsonResponse(w, map[string]interface{}{
		"success":        true,
		"chunksReceived": received,
	})
}

func jsonResponse(w http.ResponseWriter, data map[string]interface{}) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(data)
}

func jsonError(w http.ResponseWriter, msg string, status int) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": false,
		"error":   msg,
	})
}

// Note: add to imports if using strings package
var _ = strings.TrimSpace

Expected JSON Response

Both endpoints must return this JSON structure on successful upload completion.

{
  "success": true,
  "fileGuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "fileName": "photo.jpg",
  "fileSize": 204800
}

Client-Side JavaScript

Initialize MultipleUpload on the client, pointing to your Go server endpoints.

<link rel="stylesheet" href="multipleupload.css" />
<div id="uploader"></div>
<script src="multipleupload.js"></script>
<script>
  var uploader = new MultipleUpload("#uploader", {
    uploadUrl: "http://localhost:8080/upload",
    chunkUploadUrl: "http://localhost:8080/upload-chunk",
    chunkSize: 2 * 1024 * 1024, // 2 MB chunks
    onFileUploaded: function (file, response) {
      console.log("Uploaded:", response.fileName, response.fileGuid);
    }
  });
</script>