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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Cap request size up front and validate each file before touching disk to keep uploads bounded.
- 2Streaming with io.Copy avoids loading whole files into memory, and every opened handle must be closed on every exit path.
- 3Namespacing paths per user and prefixing with a timestamp prevents collisions and cross-user overwrites.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/handling-multi-file-uploads-in-go-explained-go-2126/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.