go 54 lines · 9 steps

Handling multi-file uploads in Go

An HTTP handler that parses a multipart form, validates each file, and streams uploads to disk.

Explained by highlit
1func handleUpload(w http.ResponseWriter, r *http.Request) {
2 if err := r.ParseMultipartForm(32 << 20); err != nil {
3 http.Error(w, "failed to parse form: "+err.Error(), http.StatusBadRequest)
4 return
5 }
6 defer r.MultipartForm.RemoveAll()
7 
8 files := r.MultipartForm.File["documents"]
9 if len(files) == 0 {
10 http.Error(w, "no files uploaded", http.StatusBadRequest)
11 return
12 }
13 
14 uploadDir := filepath.Join("uploads", r.FormValue("user_id"))
15 if err := os.MkdirAll(uploadDir, 0o755); err != nil {
16 http.Error(w, "storage unavailable", http.StatusInternalServerError)
17 return
18 }
19 
20 saved := make([]string, 0, len(files))
21 for _, header := range files {
22 if header.Size > 10<<20 {
23 http.Error(w, header.Filename+" exceeds 10MB limit", http.StatusRequestEntityTooLarge)
24 return
25 }
26 
27 src, err := header.Open()
28 if err != nil {
29 http.Error(w, "cannot read "+header.Filename, http.StatusBadRequest)
30 return
31 }
32 
33 name := fmt.Sprintf("%d-%s", time.Now().UnixNano(), filepath.Base(header.Filename))
34 dst, err := os.Create(filepath.Join(uploadDir, name))
35 if err != nil {
36 src.Close()
37 http.Error(w, "cannot persist file", http.StatusInternalServerError)
38 return
39 }
40 
41 if _, err := io.Copy(dst, src); err != nil {
42 src.Close()
43 dst.Close()
44 http.Error(w, "write failed", http.StatusInternalServerError)
45 return
46 }
47 src.Close()
48 dst.Close()
49 saved = append(saved, name)
50 }
51 
52 w.Header().Set("Content-Type", "application/json")
53 json.NewEncoder(w).Encode(map[string]any{"stored": saved})
54}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Cap request size up front and validate each file before touching disk to keep uploads bounded.
  2. 2Streaming with io.Copy avoids loading whole files into memory, and every opened handle must be closed on every exit path.
  3. 3Namespacing paths per user and prefixing with a timestamp prevents collisions and cross-user overwrites.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Handling multi-file uploads in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code