go
73 lines · 8 steps
Deduplicating concurrent requests in Gin
A Gin middleware that collapses identical in-flight GET requests into one handler execution using singleflight.
Explained by
highlit
1package middleware
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "net/http"
8
9 "github.com/gin-gonic/gin"
10 "golang.org/x/sync/singleflight"
11)
12
13type cachedResponse struct {
14 Status int
15 Header http.Header
16 Body []byte
17}
18
19type bufferedWriter struct {
20 gin.ResponseWriter
21 body []byte
22}
23
24func (w *bufferedWriter) Write(b []byte) (int, error) {
25 w.body = append(w.body, b...)
26 return w.ResponseWriter.Write(b)
27}
28
29func Singleflight() gin.HandlerFunc {
30 var group singleflight.Group
31
32 return func(c *gin.Context) {
33 if c.Request.Method != http.MethodGet {
34 c.Next()
35 return
36 }
37
38 sum := sha256.Sum256([]byte(c.Request.Method + "\x00" + c.Request.URL.RequestURI()))
39 key := hex.EncodeToString(sum[:])
40
41 result, err, shared := group.Do(key, func() (interface{}, error) {
42 bw := &bufferedWriter{ResponseWriter: c.Writer}
43 c.Writer = bw
44 c.Next()
45
46 return &cachedResponse{
47 Status: c.Writer.Status(),
48 Header: c.Writer.Header().Clone(),
49 Body: bw.body,
50 }, nil
51 })
52 if err != nil {
53 c.AbortWithStatus(http.StatusInternalServerError)
54 return
55 }
56
57 res := result.(*cachedResponse)
58 c.Set("singleflight.shared", shared)
59
60 if shared {
61 for k, vs := range res.Header {
62 for _, v := range vs {
63 c.Writer.Header().Add(k, v)
64 }
65 }
66 c.Writer.WriteHeader(res.Status)
67 _, _ = c.Writer.Write(res.Body)
68 c.Abort()
69 }
70 }
71}
72
73var _ = json.Marshal
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1singleflight lets many concurrent callers share the result of a single expensive computation keyed by a stable identifier.
- 2Wrapping the ResponseWriter lets middleware capture the handler's output so it can be replayed to other waiters.
- 3Coalescing only makes sense for idempotent, side-effect-free requests like GETs keyed on method and URL.
Related explainers
javascript
import { NextResponse } from 'next/server'; const locales = ['en', 'fr', 'de', 'es']; const defaultLocale = 'en';
Locale routing with Next.js middleware
middleware
i18n
content-negotiation
Intermediate
10 steps
javascript
const express = require('express'); const cookieParser = require('cookie-parser'); const router = express.Router();
Remember-me login with signed cookies in Express
authentication
signed-cookies
sessions
Intermediate
9 steps
go
package health import ( "context"
Building a health check endpoint in Go
health-check
context-timeout
dependency-probing
Intermediate
8 steps
go
package email import ( "errors"
Normalizing and deduping email addresses in Go
validation
normalization
deduplication
Intermediate
8 steps
rust
use std::time::Duration; use axum::{ http::{header, HeaderValue, Request},
Serving fingerprinted assets in Axum
static-assets
http-caching
middleware
Intermediate
7 steps
go
type PostCursor struct { CreatedAt time.Time ID int64 }
Keyset pagination with cursors in Go
pagination
keyset-cursor
database
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/deduplicating-concurrent-requests-in-gin-explained-go-7b7b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.