go
66 lines · 8 steps
Building a response cache middleware in Gin
A Gin middleware that serves cached JSON on a hit and captures fresh responses on a miss, all guarded by a read-write mutex.
Explained by
highlit
1package middleware
2
3import (
4 "bytes"
5 "net/http"
6 "sync"
7 "time"
8
9 "github.com/gin-gonic/gin"
10)
11
12type cacheEntry struct {
13 status int
14 body []byte
15 expiresAt time.Time
16}
17
18type responseCache struct {
19 mu sync.RWMutex
20 entries map[string]cacheEntry
21}
22
23var store = &responseCache{entries: make(map[string]cacheEntry)}
24
25type capturingWriter struct {
26 gin.ResponseWriter
27 buf *bytes.Buffer
28}
29
30func (w capturingWriter) Write(b []byte) (int, error) {
31 w.buf.Write(b)
32 return w.ResponseWriter.Write(b)
33}
34
35func CacheJSON(ttl time.Duration) gin.HandlerFunc {
36 return func(c *gin.Context) {
37 key := c.Request.URL.RequestURI()
38
39 store.mu.RLock()
40 entry, ok := store.entries[key]
41 store.mu.RUnlock()
42
43 if ok && time.Now().Before(entry.expiresAt) {
44 c.Header("X-Cache", "HIT")
45 c.Data(entry.status, "application/json; charset=utf-8", entry.body)
46 c.Abort()
47 return
48 }
49
50 writer := capturingWriter{ResponseWriter: c.Writer, buf: &bytes.Buffer{}}
51 c.Writer = writer
52 c.Header("X-Cache", "MISS")
53
54 c.Next()
55
56 if status := c.Writer.Status(); status == http.StatusOK && writer.buf.Len() > 0 {
57 store.mu.Lock()
58 store.entries[key] = cacheEntry{
59 status: status,
60 body: writer.buf.Bytes(),
61 expiresAt: time.Now().Add(ttl),
62 }
63 store.mu.Unlock()
64 }
65 }
66}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Embedding gin.ResponseWriter lets you intercept writes while delegating the rest of the interface for free.
- 2A sync.RWMutex lets many requests read the cache concurrently while serializing the rare write.
- 3Middleware can short-circuit with c.Abort() on a hit, or run c.Next() and inspect the result to populate the cache.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
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/building-a-response-cache-middleware-in-gin-explained-go-9718/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.