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

Walkthrough

Space play step click any line
Three takeaways
  1. 1singleflight lets many concurrent callers share the result of a single expensive computation keyed by a stable identifier.
  2. 2Wrapping the ResponseWriter lets middleware capture the handler's output so it can be replayed to other waiters.
  3. 3Coalescing only makes sense for idempotent, side-effect-free requests like GETs keyed on method and URL.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Deduplicating concurrent requests in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code