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
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 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.