Build Network Proxies and Reverse Proxies in Go: A Hands-On Guide
Source: Dev.to
Why Go for Proxies?
Go is a powerhouse for network programming:
| Feature | Why It Rocks for Proxies |
|---|---|
net/http package | Streamlines HTTP request/response handling |
| Goroutines | Scales effortlessly for concurrent requests |
| Single binary | Deploy anywhere with zero hassle |
What You’ll Learn
- The difference between forward and reverse proxies.
- How to build both in Go with clear, production‑ready code.
- Optimization tricks and real‑world lessons from my 10 years of Go experience.
Let’s dive in!
Network Proxies vs. Reverse Proxies: The Basics
Before we code, let’s clarify what proxies do. They’re middlemen in network communication, but their roles differ.
Forward Proxy – Your Client’s Advocate
A forward proxy sits between a client (e.g., your browser) and the internet, fetching resources on the client’s behalf. Think of it as a personal assistant who grabs your coffee order without revealing you’re the one asking.
- How It Works:
Client → Proxy → Server → Proxy → Client - Use Cases: Anonymity (VPNs), content filtering, caching
- Go’s Edge:
http.Clientsimplifies forwarding; Goroutines handle concurrent clients.
Reverse Proxy – The Server’s Gatekeeper
A reverse proxy sits in front of backend servers, routing client requests and shielding the backend. It’s like a restaurant host who directs your order to the right chef.
- How It Works:
Client → Reverse Proxy → Backend → Reverse Proxy → Client - Use Cases: Load balancing (Nginx), security, API gateways
- Go’s Edge:
httputil.ReverseProxymakes routing a snap; Goroutines scale traffic.
Forward vs. Reverse – Quick Comparison
| Feature | Forward Proxy | Reverse Proxy |
|---|---|---|
| Role | Serves clients | Serves backends |
| Control | Client‑configured | Server‑managed |
| Purpose | Hides client identity | Hides backend details |
| Go Tools | http.Client | httputil.ReverseProxy |
Segment 2: Building a Forward Proxy in Go
Let’s get hands‑on with a simple HTTP forward proxy. This code forwards client requests to any target server and returns the response—perfect for anonymity or caching!
Simple Forward Proxy in Go
package main
import (
"io"
"log"
"net/http"
)
func handleProxy(w http.ResponseWriter, r *http.Request) {
// Create a client that will forward the request
client := &http.Client{}
// Build a new request based on the incoming one
req, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// Copy request headers
for k, v := range r.Header {
req.Header[k] = v
}
// Forward the request
resp, err := client.Do(req)
if err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Copy response headers and status code
for k, v := range resp.Header {
w.Header()[k] = v
}
w.WriteHeader(resp.StatusCode)
// Stream the response body to the client
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/", handleProxy)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Run It
-
Save the file as
proxy.go. -
Run
go run proxy.go. -
Test with:
curl -x http://localhost:8080 http://example.com
What’s Happening
http.Clientsends the client’s request to the target.- Headers and body are copied to preserve the original request.
io.Copystreams the response efficiently.defer resp.Body.Close()prevents file‑descriptor leaks.
Pro Tip – Always close resp.Body. Forgetting this can exhaust file descriptors in production.
Optimization Tricks
In a real‑world content‑filtering proxy, I boosted performance with these tweaks:
Connection Pooling
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
Timeout: 30 * time.Second,
}
Result: Latency dropped from ~200 ms to ~50 ms by reusing connections.
Pitfall Fix – Set MaxIdleConnsPerHost to limit per‑host connections and avoid excessive TCP handshakes.
Segment 3: Building a Reverse Proxy in Go
Now, let’s build a reverse proxy with round‑robin load balancing to distribute requests across multiple backends. Ideal for microservices or high‑traffic apps!
Simple Reverse Proxy with Round‑Robin
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
)
// ReverseProxy holds the backend URLs and the current index.
type ReverseProxy struct {
backends []*url.URL
current uint64
}
// NewReverseProxy parses the backend URLs and returns a proxy instance.
func NewReverseProxy(backendURLs []string) *ReverseProxy {
urls := make([]*url.URL, len(backendURLs))
for i, u := range backendURLs {
parsed, err := url.Parse(u)
if err != nil {
log.Fatalf("Invalid backend URL %q: %v", u, err)
}
urls[i] = parsed
}
return &ReverseProxy{backends: urls}
}
// getNextBackend returns the next backend URL using atomic round‑robin.
func (p *ReverseProxy) getNextBackend() *url.URL {
idx := atomic.AddUint64(&p.current, 1)
return p.backends[idx%uint64(len(p.backends))]
}
// ServeHTTP satisfies http.Handler and forwards the request.
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
target := p.getNextBackend()
// Create a reverse proxy for the selected backend.
proxy := httputil.NewSingleHostReverseProxy(target)
// Optionally modify the request before sending it upstream.
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// Preserve the original Host header if you need it.
req.Host = target.Host
}
proxy.ServeHTTP(w, r)
}
func main() {
// Example backends – replace with your own services.
backends := []string{
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
}
proxy := NewReverseProxy(backends)
log.Println("Reverse proxy listening on :8080")
if err := http.ListenAndServe(":8080", proxy); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
How It Works
- Round‑Robin Selection –
getNextBackendatomically picks the next backend URL. httputil.NewSingleHostReverseProxy– Handles request rewriting, response copying, and connection reuse.- Custom Director – Allows you to tweak the outbound request (e.g., preserve the original
Hostheader).
Test It
# Start three simple backend servers (e.g., using Python's http.server)
python3 -m http.server 8081 &
python3 -m http.server 8082 &
python3 -m http.server 8083 &
# Run the reverse proxy
go run reverse_proxy.go
# Send requests
curl http://localhost:8080
curl http://localhost:8080
curl http://localhost:8080
You’ll see the requests being served by the three backends in turn.
Pro Tip – For production, consider adding health‑checks, graceful shutdown, and TLS termination.
Wrapping Up
Proxies are a great playground for mastering Go’s networking stack. With the forward‑proxy example you now have a foundation for anonymity or content filtering, and the reverse‑proxy with round‑robin load balancing shows how to build a lightweight API gateway or edge router.
Feel free to fork the snippets, experiment with TLS, add caching layers, or integrate metrics (Prometheus, OpenTelemetry). Happy coding, and may your Go routines never block! 🚀
Segment 3: Simple Reverse Proxy in Go
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
)
type ReverseProxy struct {
backends []*url.URL
counter uint64
}
func NewReverseProxy(backends []string) *ReverseProxy {
urls := make([]*url.URL, len(backends))
for i, b := range backends {
u, err := url.Parse(b)
if err != nil {
log.Fatalf("invalid backend URL %s: %v", b, err)
}
urls[i] = u
}
return &ReverseProxy{backends: urls}
}
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Round‑robin selection
index := atomic.AddUint64(&p.counter, 1) % uint64(len(p.backends))
proxy := httputil.NewSingleHostReverseProxy(p.backends[index])
proxy.ServeHTTP(w, r)
}
func main() {
backends := []string{"http://localhost:8081", "http://localhost:8082"}
proxy := NewReverseProxy(backends)
log.Fatal(http.ListenAndServe(":8080", proxy))
}
Run It
- Save the file as
reverse_proxy.go. - Run mock backends on ports 8081 and 8082 (e.g., simple Go HTTP servers).
- Execute
go run reverse_proxy.go. - Test with
curl http://localhost:8080.
What’s Happening
httputil.ReverseProxyhandles request forwarding.atomic.AddUint64ensures thread‑safe round‑robin selection, so requests alternate between backends.
Pro Tip – Enable connection reuse:
proxy.Transport = &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
Real‑World Insights
Health Checks
func (p *ReverseProxy) healthCheck() {
for {
for _, backend := range p.backends {
resp, err := http.Get(backend.String() + "/health")
// Update backend status based on err / resp.StatusCode
if resp != nil {
resp.Body.Close()
}
}
time.Sleep(10 * time.Second)
}
}
Pitfall Fix
Set ResponseHeaderTimeout to avoid slow‑backend delays.
Advanced Features and Best Practices
Let’s level up with advanced features like concurrency, security, and monitoring, plus best practices to make your proxy production‑ready.
High Concurrency
Go’s goroutines shine here. Each request runs in its own lightweight thread, handling thousands of connections with minimal memory. For dynamic backends, use a thread‑safe manager:
type BackendManager struct {
backends []*url.URL
mu sync.RWMutex
}
func (m *BackendManager) UpdateBackends(newBackends []string) {
m.mu.Lock()
defer m.mu.Unlock()
urls := make([]*url.URL, len(newBackends))
for i, u := range newBackends {
urls[i], _ = url.Parse(u)
}
m.backends = urls
}
Insight – Pair with Consul for zero‑downtime backend updates in Kubernetes.
Security
- TLS – Use
http.ListenAndServeTLSwith Let’s Encrypt viagolang.org/x/crypto/acme/autocert. - Rate Limiting – Mitigate DDoS with
golang.org/x/time/rate:
limiter := rate.NewLimiter(10, 50) // 10 reqs/sec, 50 burst
if !limiter.Allow() {
http.Error(w, "Rate Limit Exceeded", http.StatusTooManyRequests)
return
}
Monitoring
- Profiling – Enable
net/http/pprofon:6060for CPU/memory insights. - Metrics – Use
prometheus/client_golangfor Prometheus/Grafana dashboards.
Pitfall Fix – Missing metrics made debugging a nightmare. Deploy Prometheus to track http_requests_total.
Best Practices
- Timeouts – Set
http.Clientandhttp.Transporttimeouts to prevent hangs. - Logging – Use
go.uber.org/zapfor structured, performant logs. - Deployment – Containerize with Docker:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o proxy
CMD ["./proxy"]
Insight – Nginx + Go for SSL termination boosted performance by ~20 %.
Real‑World Use Cases and Call to Action
API Gateway
Route requests to microservices with authentication:
mux := http.NewServeMux()
mux.Handle("/users/", httputil.NewSingleHostReverseProxy(userService))
mux.Handle("/orders/", httputil.NewSingleHostReverseProxy(orderService))
Tip – Use http.StripPrefix to avoid routing conflicts.
Load Balancer
Use consistent hashing (github.com/stathat/consistent) for better cache hits and dynamic health checks.
Caching Proxy
Cache static content with sync.Map and TTL:
type CacheEntry struct {
Data []byte
ExpiresAt time.Time
}
p.cache.Store(key, CacheEntry{
Data: data,
ExpiresAt: time.Now().Add(5 * time.Minute),
})
Insight – This boosted cache‑hit rates from 60 % to 85 %.
Key Takeaways
- Go’s
net/httpandhttputilmake proxy development straightforward. - Goroutines and connection pooling handle high traffic with ease.
- Optimizations like health checks and TLS ensure reliability.
Call to Action
Build your own proxy! Try one of the following:
- A TLS‑enabled forward proxy with Let’s Encrypt.
- A reverse proxy with Prometheus monitoring.
Share your projects in the comments—I’d love to see what you create! Have questions or hit a snag? Drop a comment, and let’s debug together. 🚀