Go: Go HTTP Clients and Middleware
Last updated: 2026-08-26
HTTP clients and وسيط are the two cornerstones of microservice communication—one manages connections, and the other handles cross-cutting concerns.
When your service needs to call five other downstream services, each call requires logging, timeout handling, retries, and circuit breaking—should you write this code five times or abstract it once? In this lesson, you'll master advanced usage of the Go HTTP عميل and وسيط design patterns.
1. You will learn
http.Get/Post/DoBasic Requestshttp.ClientTimeout Controlhttp.Transportconnection pool configuration- Middleware Pattern (Onion Model)
- Common وسيط: logging, recovery, CORS, timeouts
httputil.ReverseProxyreverse proxy
2. A True Story of a Backend Engineer
(1) Pain Point: Five downstream services, each requiring logging, timeout handling, and retries
Charlie is in charge of the company's API gateway service, which needs to call five downstream services:
"Every downstream call must: print a طلب log (to facilitate troubleshooting), set a timeout (to prevent deadlocks), and record the duration (for monitoring and alerts). I ended up writing the same code five times—one دالة per service, each containing the same repetitive 'sandwich' of logging, timeout, and the call itself."
His code at the time:
// Bad code: every downstream service repeats the same logic
func callUserService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] Requesting user service: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://user-service/api/users")
log.Printf("[%s] User service elapsed: %v", r.Method, time.Since(start))
// ... process response
}
func callOrderService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] Requesting order service: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://order-service/api/orders")
log.Printf("[%s] Order service elapsed: %v", r.Method, time.Since(start))
// ... same pattern again!
}
// Every new service repeats!
(2) Go Solution: Middleware Pattern + Custom Client
// Middleware: wraps http.RoundTripper
type LoggingMiddleware struct {
next http.RoundTripper
}
func (m *LoggingMiddleware) RoundTrip(req *http.Request) (*http.Response, خطأ) {
start := time.Now()
log.Printf("[%s] %s %s", req.Method, req.URL.Host, req.URL.Path)
resp, err := m.next.RoundTrip(req)
log.Printf("[%s] %s elapsed: %v", req.Method, req.URL.Path, time.Since(start))
return resp, err
}
// Unified Client: shared by all downstream services
عميل := &http.Client{
Timeout: 5 * time.Second,
Transport: &LoggingMiddleware{
next: http.DefaultTransport,
},
}
// All downstream calls automatically get logging + timeout
resp1, _ := عميل.Get("http://user-service/api/users") // Auto logging
resp2, _ := عميل.Get("http://order-service/api/orders") // Auto logging
(3) Benefits: Before and After Middleware Implementation
| Dimension | Duplicate Code | Middleware Pattern |
|---|---|---|
| Code Volume | 30 lines per downstream, 5 downstreams = 150 lines | 15 lines of middleware, for a total of 30 lines |
| Add downstream | Copy and paste 30 lines | Call client.Get directly |
| Edit timeout | Change 5 places | Change 1 client.Timeout |
3. HTTP Client Basics
▶ Example: GET / POST / Custom Requests
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
// 1. GET request
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("GET status: %d\n", resp.StatusCode)
fmt.Printf("Response: %s\n", body)
// 2. POST JSON request
data := map[string]string{"name": "Alice"}
jsonData, _ := json.Marshal(data)
resp, err = http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewReader(jsonData),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 3. Custom request (set Headers)
req, _ := http.NewRequest("DELETE", "https://api.example.com/users/1", nil)
req.Header.Set("Authorization", "Bearer token-123")
req.Header.Set("X-Request-ID", "req-456")
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("DELETE status: %d\n", resp.StatusCode)
}
(2) http.Get vs http.Client
// Method 1: Use http.Get directly (default Client)
resp, _ := http.Get(url)
// Problem: No default timeout, may hang forever!
// Method 2: Custom Client (recommended)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, _ := client.Get(url)
// Safe: 10-second timeout auto-cancels
| Method | Timeout | Connection Pool | Recommendation |
|---|---|---|---|
http.Get(url) |
No timeout | Default (limited reuse) | ❌ For quick testing only |
http.DefaultClient |
No timeout | Default | ⚠️ Use with caution in production |
&http.Client{Timeout: 10s} |
Global timeout | Default | ✅ Recommended |
&http.Client{Timeout, Transport} |
Global timeout | Custom connection pool | ✅ Production-ready |
4. http.Client Timeouts and Transport
▶ Example: Configuring Timeouts and Connection Pools
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// Custom Transport
transport := &http.Transport{
// Connection pool
MaxIdleConns: 100, // Max idle connections
MaxIdleConnsPerHost: 10, // Max idle connections per host
IdleConnTimeout: 90 * time.Second, // Idle connection timeout
// TLS
TLSHandshakeTimeout: 10 * time.Second,
// Dial
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // Connection timeout
KeepAlive: 30 * time.Second, // Keep-Alive interval
}).DialContext,
}
// Custom Client
عميل := &http.Client{
Timeout: 30 * time.Second, // Total طلب timeout (includes all stages)
Transport: transport,
}
// Use عميل to send طلب
resp, err := عميل.Get("https://api.example.com/users")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
}
▶ Example: Tiered Timeout Control
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// Timeout tiers:
// Transport.DialContext → Connection timeout (10s)
// Transport.TLSHandshake → TLS handshake timeout (5s)
// Transport.ResponseHeader → Response header timeout (10s)
// Client.Timeout → Total timeout (30s, covers Dial + TLS + send + read)
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
start := time.Now()
resp, err := client.Get("https://httpbin.org/delay/5")
if err != nil {
fmt.Printf("Error: %v (elapsed: %v)\n", err, time.Since(start))
return
}
defer resp.Body.Close()
fmt.Printf("Success: %d (elapsed: %v)\n", resp.StatusCode, time.Since(start))
}
(3) Transport Configuration Parameters
| Parameter | Default | Recommended | Description |
|---|---|---|---|
MaxIdleConns |
100 | 100–200 | Global maximum number of idle connections |
MaxIdleConnsPerHost |
2 | 10–50 | Maximum number of idle connections per host (the default of 2 is too low!) |
IdleConnTimeout |
90s | 30–90s | Idle connection timeout |
TLSHandshakeTimeout |
10s | 5–10s | TLS handshake timeout |
ResponseHeaderTimeout |
0 (Never) | 10–30s | Response header timeout |
DialContext.Timeout |
None | 10–30s | TCP connection timeout |
MaxIdleConnsPerHost in http.Transport is only 2, which is severely insufficient for scenarios involving high-concurrency calls to the same service. Be sure to increase this value based on the level of concurrency (e.g., 50–100).
5. Middleware Pattern
▶ Example: The Onion Model of Middleware
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
// Middleware type: receives Handler, returns Handler
type Middleware func(http.Handler) http.Handler
// Middleware chain: assembles all middleware into one
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ---------- Concrete middleware ----------
// 1. Logging middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s elapsed: %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 2. Recovery middleware (prevents panic from crashing the service)
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 3. CORS middleware
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 4. Timeout middleware
func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---------- Usage ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
// Chain all middleware (onion model: Recovery → Logging → CORS → Handler)
handler := Chain(mux,
RecoveryMiddleware,
LoggingMiddleware,
CORSMiddleware,
TimeoutMiddleware(5*time.Second),
)
log.Print(http.ListenAndServe(":8080", handler))
}
graph LR
Req[Request] --> R[Recovery]
R --> L[Logging]
L --> C[CORS]
C --> T[Timeout]
T --> H[Handler]
H -->|Response| T
T --> C
C --> L
L --> R
R --> Resp[Response]
(2) The Responsibilities of the Four Core Middleware Components
| Middleware | Responsibilities | Order |
|---|---|---|
| Recovery | Catch panic, return 500 | Outermost |
| Logging | Logging Request and Response Times | Layer 2 |
| CORS | Handling Cross-Origin Request Headers | Layer 3 |
| Timeout | Request Timeout Control | Innermost Layer (Close to Handler) |
6. httputil.ReverseProxy Reverse Proxy
▶ Example: Reverse Proxy
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// Target service address
target, _ := url.Parse("http://localhost:8081")
// Create reverse proxy
proxy := httputil.NewSingleHostReverseProxy(target)
// Custom Director (modify طلب headers)
proxy.Director = func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path + req.URL.Path
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Header.Set("X-Real-IP", req.RemoteAddr)
}
// Custom خطأ handling
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err خطأ) {
log.Printf("Proxy خطأ: %v", err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
mux := http.NewServeMux()
mux.HandleFunc("/api/", proxy.ServeHTTP)
log.Print("API gateway started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
httputil.ReverseProxy automatically handles most proxy details: Host header forwarding, X-Forwarded-For, response header passing, and connection pool reuse. All you need to do is provide a Director function to modify the request.
7. Complete Example: API Gateway + Middleware Pipeline
// api_gateway.go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"time"
)
// ---------- Configuration ----------
type Route struct {
Path string
Target *url.URL
}
type GatewayConfig struct {
Port string
Routes []Route
Middlewares []Middleware
}
// ---------- Middleware ----------
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// Logging
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Recovery
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Rate limiting (simplified token bucket)
type RateLimiter struct {
tokens chan struct{}
}
func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{tokens: make(chan struct{}, rate)}
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
// Refill every second
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-rl.tokens:
next.ServeHTTP(w, r)
default:
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
}
})
}
// ---------- Gateway ----------
type Gateway struct {
config GatewayConfig
proxy *httputil.ReverseProxy
}
func NewGateway(config GatewayConfig) *Gateway {
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// Match route based on request path
for _, route := range config.Routes {
if strings.HasPrefix(req.URL.Path, route.Path) {
req.URL.Scheme = route.Target.Scheme
req.URL.Host = route.Target.Host
req.URL.Path = strings.TrimPrefix(req.URL.Path, route.Path)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
return
}
}
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Proxy error: %v", err)
http.Error(w, `{"error":"bad gateway"}`, http.StatusBadGateway)
},
}
return &Gateway{config: config, proxy: proxy}
}
func (g *Gateway) Start() error {
routesJSON, _ := json.MarshalIndent(g.config.Routes, "", " ")
log.Printf("Loading routes:\n%s\n", routesJSON)
// Route distribution
mux := http.NewServeMux()
for _, route := range g.config.Routes {
pattern := route.Path
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
mux.Handle(pattern, g.proxy)
}
// Health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Middleware chain
handler := Chain(mux, g.config.Middlewares...)
server := &http.Server{
Addr: ":" + g.config.Port,
Handler: handler,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
<-sigCh
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Printf("API gateway started on :%s", g.config.Port)
return server.ListenAndServe()
}
func main() {
userService, _ := url.Parse("http://localhost:8081")
orderService, _ := url.Parse("http://localhost:8082")
config := GatewayConfig{
Port: "8080",
Routes: []Route{
{Path: "/api/users", Target: userService},
{Path: "/api/orders", Target: orderService},
},
}
config.Middlewares = []Middleware{
Recovery,
Logging,
CORS,
}
gateway := NewGateway(config)
if err := gateway.Start(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
httputil.ReverseProxy modifies the X-Forwarded-For header by default. If you don't want this behavior, remove the header in Director. Additionally, ReverseProxy does not automatically handle WebSocket upgrades—the ServeHTTP طريقة of httputil.ReverseProxy does not support WebSockets, so you must handle the upgrade طلب separately.
❓ FAQ
http.Get and http.Client?http.Get for quick testing. In a production environment, always use a custom http.Client—set the Timeout to prevent requests from hanging, and configure Transport to control connection pooling and timeout details. http.Get uses http.DefaultClient, which has no default timeout.IdleConnTimeout to 30–90 seconds to keep connections active.Chain(h, A, B, C) → Requests pass through A → B → C → Handler; responses pass through C → B → A.Director دالة to modify the طلب's Scheme, Host, and Path. It is suitable as a traffic entry point for microservice architectures.http.Client خيط-safe?http.Client can be used concurrently (multiple calls to Get or Do do not require additional locks). However, the Transport fields should not be modified after the first use. Best practice: Create one Client for each downstream service cluster; do not create a new Client for every طلب.http.NewRequest, set the header as follows: req.Header.Set("Key", "Value"). Do not use req.Header.Add("Key", "Value")—Add appends rather than overwrites. Common headers such as Auth, TraceID, and Content-Type are all passed in the طلب headers.📖 Summary
- The three types of requests: HTTP GET, POST, and DO
- Setting a timeout for http.Client to prevent it from hanging
- http.Transport connection pool configuration (Increase the value of MaxIdleConnsPerHost)
- Middleware pattern: Onion model, where requests travel from the outer layer to the inner layer
- 4 core وسيط components: Recovery → Logging → CORS → Timeout
- httputil.ReverseProxy implements a reverse proxy
- API Gateway integrates the وسيط chain + routing and distribution
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Write a custom
http.Clientwith a 5-second timeout andMaxIdleConnsPerHost=20. Use thisClientto send a GET طلب to https://httpbin.org/delay/3 and verify that the timeout works as expected. -
Advanced Problem (Difficulty ⭐⭐): Implement a طلب retry وسيط (RoundTripper layer). Requirements: (1) Automatically retry GET requests up to 3 times; (2) Set the retry intervals to 100 ms, 200 ms, and 400 ms, respectively (exponential backoff); (3) Retry only 5xx responses; do not retry 4xx responses; (4) Log each retry attempt.
-
Challenge (Difficulty: ⭐⭐⭐): Implement a reverse proxy with load balancing. Requirements: (1) Support registration of multiple واجهة خلفية instances (e.g., 3 user-service instances); (2) Distribute requests using the Round-Robin algorithm; (3) Perform passive health checks—remove an مثيل after 3 consecutive failures and re-add it once it recovers; (4) Log the واجهة خلفية address to which each طلب is routed; (5) Implement using
httputil.ReverseProxy.