go
75 lines · 8 steps
Region-aware upstream routing in Gin
A Gin middleware picks the lowest-latency backend for each request's region, refreshing its routing table in the background.
Explained by
highlit
1package routing
2
3import (
4 "net/http"
5 "strings"
6 "sync"
7 "time"
8
9 "github.com/gin-gonic/gin"
10)
11
12type Upstream struct {
13 Region string
14 BaseURL string
15 Latency time.Duration
16}
17
18type RegionRouter struct {
19 mu sync.RWMutex
20 byRegion map[string][]Upstream
21 fallback string
22}
23
24func NewRegionRouter(registry UpstreamRegistry, fallback string) *RegionRouter {
25 r := &RegionRouter{byRegion: make(map[string][]Upstream), fallback: fallback}
26 r.warm(registry)
27 go func() {
28 for range time.Tick(30 * time.Second) {
29 r.warm(registry)
30 }
31 }()
32 return r
33}
34
35func (r *RegionRouter) warm(registry UpstreamRegistry) {
36 snapshot := make(map[string][]Upstream)
37 for _, u := range registry.Discover() {
38 snapshot[u.Region] = append(snapshot[u.Region], u)
39 }
40 r.mu.Lock()
41 r.byRegion = snapshot
42 r.mu.Unlock()
43}
44
45func (r *RegionRouter) closest(region string) (Upstream, bool) {
46 r.mu.RLock()
47 candidates := r.byRegion[region]
48 r.mu.RUnlock()
49 if len(candidates) == 0 {
50 return Upstream{}, false
51 }
52 best := candidates[0]
53 for _, u := range candidates[1:] {
54 if u.Latency < best.Latency {
55 best = u
56 }
57 }
58 return best, true
59}
60
61func (r *RegionRouter) Middleware() gin.HandlerFunc {
62 return func(c *gin.Context) {
63 region := strings.ToUpper(strings.TrimSpace(c.GetHeader("CF-IPCountry")))
64 upstream, ok := r.closest(region)
65 if !ok {
66 if upstream, ok = r.closest(r.fallback); !ok {
67 c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "no upstream available"})
68 return
69 }
70 }
71 c.Set("upstream", upstream)
72 c.Header("X-Served-Region", upstream.Region)
73 c.Next()
74 }
75}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Swapping an entire snapshot map under a write lock keeps reads lock-free and consistent without mutating shared state in place.
- 2A background ticker lets you serve from a warm cache while keeping routing data fresh, decoupling discovery cost from request latency.
- 3Middleware is the right seam for cross-cutting request routing decisions, attaching results to the context for downstream handlers.
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
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 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
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
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/region-aware-upstream-routing-in-gin-explained-go-8ec4/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.