go
63 lines · 9 steps
Deduping in-flight requests in Gin
A middleware that collapses identical concurrent requests so only one runs while the rest wait for and share its response.
Explained by
highlit
1package handler
2
3type flightResult struct {
4 status int
5 body []byte
6}
7
8type inflightEntry struct {
9 done chan struct{}
10 result flightResult
11}
12
13type flightRegistry struct {
14 mu sync.Mutex
15 pending map[string]*inflightEntry
16}
17
18var registry = &flightRegistry{pending: make(map[string]*inflightEntry)}
19
20func Dedupe() gin.HandlerFunc {
21 return func(c *gin.Context) {
22 key := c.Request.Method + " " + c.FullPath() + "?" + c.Request.URL.RawQuery
23
24 registry.mu.Lock()
25 if entry, ok := registry.pending[key]; ok {
26 registry.mu.Unlock()
27 <-entry.done
28 c.Header("X-Deduped", "1")
29 c.Data(entry.result.status, "application/json", entry.result.body)
30 c.Abort()
31 return
32 }
33 entry := &inflightEntry{done: make(chan struct{})}
34 registry.pending[key] = entry
35 registry.mu.Unlock()
36
37 rec := &bodyRecorder{ResponseWriter: c.Writer, buf: &bytes.Buffer{}}
38 c.Writer = rec
39
40 c.Next()
41
42 entry.result = flightResult{status: rec.Status(), body: rec.buf.Bytes()}
43 close(entry.done)
44
45 time.AfterFunc(2*time.Second, func() {
46 registry.mu.Lock()
47 if registry.pending[key] == entry {
48 delete(registry.pending, key)
49 }
50 registry.mu.Unlock()
51 })
52 }
53}
54
55type bodyRecorder struct {
56 gin.ResponseWriter
57 buf *bytes.Buffer
58}
59
60func (r *bodyRecorder) Write(b []byte) (int, error) {
61 r.buf.Write(b)
62 return r.ResponseWriter.Write(b)
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A shared map guarded by a mutex plus a done channel lets many goroutines wait on one in-flight computation.
- 2Wrapping the ResponseWriter captures the response body so it can be replayed to duplicate callers.
- 3Cleaning up the registry entry after a short delay bounds how long results are shared without leaking memory.
Related explainers
rust
use axum::{ body::{Body, Bytes}, extract::Request, http::StatusCode,
Logging request and response sizes in Axum
middleware
http
streaming-bodies
Advanced
8 steps
go
func (h *ExportHandler) StreamExport(c *gin.Context) { datasetID := c.Param("id") ctx := c.Request.Context()
Streaming NDJSON progress with Gin
streaming
goroutines
channels
Advanced
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
javascript
const express = require('express'); const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware'); const router = express.Router();
Building an API gateway proxy in Express
reverse-proxy
middleware
api-gateway
Intermediate
6 steps
go
package middleware import ( "net/http"
Per-IP write rate limiting in Gin
rate-limiting
middleware
concurrency
Intermediate
8 steps
python
import asyncio from dataclasses import dataclass import aiohttp
Bounded-concurrency HTTP fetching with asyncio
async
concurrency
semaphore
Intermediate
8 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/deduping-in-flight-requests-in-gin-explained-go-262a/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.