Day 37 — DNS in Go
Learning Objectives
- Resolve hostnames with TTL-aware caching
- Handle multi-IP responses and failover
- Measure DNS latency separately — it's often the hidden bottleneck
Exercise: DNS-Aware Connection Dialer
package main
import (
"fmt"
"net"
"sync"
"time"
)
type DNSCache struct {
mu sync.RWMutex
entries map[string]cacheEntry
}
type cacheEntry struct {
addrs []string
expiry time.Time
}
var dnsCache = &DNSCache{entries: make(map[string]cacheEntry)}
func (c *DNSCache) Lookup(host string) ([]string, error) {
c.mu.RLock()
if entry, ok := c.entries[host]; ok && time.Now().Before(entry.expiry) {
c.mu.RUnlock()
return entry.addrs, nil
}
c.mu.RUnlock()
// Cache miss — resolve
start := time.Now()
addrs, err := net.LookupHost(host)
dnsLatency := time.Since(start)
fmt.Printf("[DNS] resolved %s → %v in %v\n", host, addrs, dnsLatency)
if err != nil {
return nil, err
}
c.mu.Lock()
c.entries[host] = cacheEntry{
addrs: addrs,
expiry: time.Now().Add(30 * time.Second), // 30s TTL (simplified)
}
c.mu.Unlock()
return addrs, nil
}
// DialWithFallback tries each resolved IP until one connects
func DialWithFallback(host, port string) (net.Conn, string, error) {
addrs, err := dnsCache.Lookup(host)
if err != nil {
return nil, "", fmt.Errorf("DNS lookup %s: %w", host, err)
}
dialer := net.Dialer{Timeout: 2 * time.Second}
var lastErr error
for _, addr := range addrs {
target := net.JoinHostPort(addr, port)
conn, err := dialer.Dial("tcp", target)
if err == nil {
return conn, addr, nil
}
lastErr = err
fmt.Printf("[DIAL] %s failed: %v, trying next\n", target, err)
}
return nil, "", fmt.Errorf("all addresses for %s failed: %w", host, lastErr)
}
func main() {
// First call: DNS miss, resolves
conn, ip, err := DialWithFallback("example.com", "80")
if err != nil {
fmt.Println("ERROR:", err)
return
}
fmt.Printf("Connected to %s (via %s)\n", "example.com", ip)
conn.Close()
// Second call: DNS cache hit
conn2, ip2, err := DialWithFallback("example.com", "80")
if err != nil {
fmt.Println("ERROR:", err)
return
}
fmt.Printf("Reconnected to %s (via %s, from cache)\n", "example.com", ip2)
conn2.Close()
}
Real-World Context
Multi-IP DNS responses are how CDNs do geographic load balancing. dig example.com returns different IPs depending on your location. Your load balancer must handle DNS changes — caching with TTL means it picks up new IPs when backends are added, and falls back to other IPs when one fails.