go
35 lines · 8 steps
Streaming NDJSON progress with Gin
A Gin handler runs an export in a goroutine and streams progress updates to the client as newline-delimited JSON.
Explained by
highlit
1func (h *ExportHandler) StreamExport(c *gin.Context) {
2 datasetID := c.Param("id")
3
4 ctx := c.Request.Context()
5 progress := make(chan ExportProgress, 8)
6
7 go func() {
8 defer close(progress)
9 if err := h.exporter.Run(ctx, datasetID, progress); err != nil {
10 progress <- ExportProgress{Stage: "failed", Error: err.Error()}
11 }
12 }()
13
14 c.Header("Content-Type", "application/x-ndjson")
15 c.Header("Cache-Control", "no-cache")
16 c.Header("X-Accel-Buffering", "no")
17
18 enc := json.NewEncoder(c.Writer)
19
20 c.Stream(func(w io.Writer) bool {
21 select {
22 case update, ok := <-progress:
23 if !ok {
24 return false
25 }
26 if err := enc.Encode(update); err != nil {
27 return false
28 }
29 c.Writer.Flush()
30 return update.Stage != "failed"
31 case <-ctx.Done():
32 return false
33 }
34 })
35}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Running work in a goroutine and reporting through a buffered channel decouples production of progress from its delivery to the client.
- 2Closing the channel with defer gives the streaming loop a clean, unambiguous end-of-stream signal.
- 3Selecting on both the progress channel and ctx.Done lets a stream stop promptly when the client disconnects.
Related explainers
javascript
import { Suspense } from 'react'; import { searchProducts } from '@/lib/products'; import SearchInput from './search-input';
Streaming search results in a Next.js Server Component
server-components
suspense
streaming
Intermediate
8 steps
go
package handler type flightResult struct { status int
Deduping in-flight requests in Gin
middleware
concurrency
deduplication
Advanced
9 steps
javascript
import { notFound } from 'next/navigation' import { Suspense } from 'react' import { getPostBySlug, getRelatedPosts } from '@/lib/posts' import { RelatedPosts } from '@/components/related-posts'
Building a dynamic blog post page in Next.js
server-components
dynamic-routing
metadata
Intermediate
8 steps
go
type CreateUserInput struct { Name string `json:"name" binding:"required,min=2,max=64"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"`
Turning Gin validation errors into JSON
validation
request-binding
error-handling
Intermediate
9 steps
go
package middleware import ( "net/http"
Per-IP write rate limiting in Gin
rate-limiting
middleware
concurrency
Intermediate
8 steps
go
package middleware import ( "net/http"
A content-type guard middleware in Gin
middleware
closures
http
Intermediate
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/streaming-ndjson-progress-with-gin-explained-go-d264/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.