go 57 lines · 8 steps

Streaming a CSV export in Go

An HTTP handler streams database rows straight into a CSV download, flushing periodically to keep memory flat.

Explained by highlit
1func (h *ReportHandler) ExportTransactions(w http.ResponseWriter, r *http.Request) {
2 ctx := r.Context()
3 
4 from, to, err := parseDateRange(r.URL.Query())
5 if err != nil {
6 http.Error(w, err.Error(), http.StatusBadRequest)
7 return
8 }
9 
10 filename := fmt.Sprintf("transactions_%s_%s.csv", from.Format("20060102"), to.Format("20060102"))
11 w.Header().Set("Content-Type", "text/csv; charset=utf-8")
12 w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
13 w.Header().Set("X-Content-Type-Options", "nosniff")
14 
15 rows, err := h.store.StreamTransactions(ctx, from, to)
16 if err != nil {
17 http.Error(w, "failed to load transactions", http.StatusInternalServerError)
18 return
19 }
20 defer rows.Close()
21 
22 cw := csv.NewWriter(w)
23 if err := cw.Write([]string{"id", "date", "account", "description", "amount"}); err != nil {
24 return
25 }
26 
27 flusher, _ := w.(http.Flusher)
28 for i := 0; rows.Next(); i++ {
29 var t Transaction
30 if err := rows.Scan(&t.ID, &t.Date, &t.Account, &t.Description, &t.Amount); err != nil {
31 log.Printf("export scan error: %v", err)
32 return
33 }
34 record := []string{
35 t.ID,
36 t.Date.Format(time.RFC3339),
37 t.Account,
38 t.Description,
39 strconv.FormatInt(t.Amount, 10),
40 }
41 if err := cw.Write(record); err != nil {
42 return
43 }
44 if i%500 == 0 {
45 cw.Flush()
46 if flusher != nil {
47 flusher.Flush()
48 }
49 }
50 }
51 
52 if err := rows.Err(); err != nil {
53 log.Printf("export iteration error: %v", err)
54 return
55 }
56 cw.Flush()
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Setting response headers before writing any body is what turns a plain response into a browser download.
  2. 2Iterating rows and flushing in batches keeps memory bounded no matter how large the result set grows.
  3. 3Once the body has started streaming you can't change the status code, so validation and header setup must happen first.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a CSV export in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code