Frontend Labs
⋯

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.


Generated from week5.md · 8 Oct 2026 16:36