1. go
  2. /standard library
  3. /http-server

Building HTTP Servers in Go

Go's net/http package provides a powerful foundation for building HTTP servers. This guide covers everything from basic servers to advanced features and best practices.

Basic HTTP Server

Simple Server

func main() {
    // Define handler
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
    })
    
    // Start server
    fmt.Println("Server starting on :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

Custom Server Configuration

func customServer() error {
    server := &http.Server{
        Addr:         ":8080",
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  15 * time.Second,
        Handler:      nil, // Use default ServeMux
    }
    
    return server.ListenAndServe()
}

Routing

Basic Routing

func setupRoutes() {
    // Root handler
    http.HandleFunc("/", homeHandler)
    
    // API routes
    http.HandleFunc("/api/users", usersHandler)
    http.HandleFunc("/api/products", productsHandler)
    
    // Serve static files
    fs := http.FileServer(http.Dir("static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    fmt.Fprintf(w, "Welcome Home!")
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // Handle GET
    case http.MethodPost:
        // Handle POST
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

Custom Router

type Router struct {
    handlers map[string]http.HandlerFunc
}

func NewRouter() *Router {
    return &Router{
        handlers: make(map[string]http.HandlerFunc),
    }
}

func (r *Router) Handle(pattern string, handler http.HandlerFunc) {
    r.handlers[pattern] = handler
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if handler, ok := r.handlers[req.URL.Path]; ok {
        handler(w, req)
        return
    }
    http.NotFound(w, req)
}

Middleware

Basic Middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Call the next handler
        next.ServeHTTP(w, r)
        
        // Log the request
        log.Printf(
            "%s %s %s",
            r.Method,
            r.RequestURI,
            time.Since(start),
        )
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        // Call the next handler
        next.ServeHTTP(w, r)
    })
}

Middleware Chain

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, middleware ...Middleware) http.Handler {
    for _, m := range middleware {
        h = m(h)
    }
    return h
}

func main() {
    handler := http.HandlerFunc(finalHandler)
    
    // Apply middleware
    chain := Chain(handler,
        loggingMiddleware,
        authMiddleware,
        // Add more middleware...
    )
    
    http.Handle("/", chain)
}

Request Handling

Reading Request Data

func handleRequest(w http.ResponseWriter, r *http.Request) {
    // Read query parameters
    name := r.URL.Query().Get("name")
    
    // Read headers
    userAgent := r.Header.Get("User-Agent")
    
    // Read cookies
    cookie, err := r.Cookie("session")
    if err != nil {
        http.Error(w, "No session", http.StatusBadRequest)
        return
    }
    
    // Read body
    var data struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    
    if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
}

Writing Response

func writeResponse(w http.ResponseWriter, r *http.Request) {
    // Set headers
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Custom-Header", "value")
    
    // Set status code
    w.WriteHeader(http.StatusOK)
    
    // Write response
    response := map[string]interface{}{
        "status": "success",
        "data": map[string]string{
            "message": "Hello, World!",
        },
    }
    
    if err := json.NewEncoder(w).Encode(response); err != nil {
        http.Error(w, "Error encoding response", http.StatusInternalServerError)
        return
    }
}

Static File Serving

Basic File Server

func setupStaticFiles() {
    // Serve entire directory
    fs := http.FileServer(http.Dir("static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    
    // Serve single file
    http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "static/favicon.ico")
    })
}

Custom File Server

func customFileServer(root string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Add custom headers
        w.Header().Set("Cache-Control", "max-age=3600")
        
        // Serve file
        http.FileServer(http.Dir(root)).ServeHTTP(w, r)
    })
}

Graceful Shutdown

Implementing Graceful Shutdown

func main() {
    server := &http.Server{
        Addr: ":8080",
        Handler: setupHandlers(),
    }
    
    // Start server in goroutine
    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("ListenAndServe(): %v", err)
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    
    // Shutdown server gracefully
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
    
    log.Println("Server exiting")
}

Best Practices

  1. Use Context for Timeouts

    func handleWithContext(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
        defer cancel()
        
        select {
        case <-ctx.Done():
            http.Error(w, "Request timeout", http.StatusGatewayTimeout)
            return
        case result := <-processRequest(ctx):
            json.NewEncoder(w).Encode(result)
        }
    }
    
  2. Handle Panic Recovery

    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. Validate Request Size

    func limitBodySize(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
            next.ServeHTTP(w, r)
        })
    }
    
  4. Use HTTPS

    func main() {
        // Redirect HTTP to HTTPS
        go http.ListenAndServe(":80", http.HandlerFunc(redirect))
        
        // HTTPS server
        log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
    }
    
    func redirect(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r,
            "https://"+r.Host+r.RequestURI,
            http.StatusMovedPermanently)
    }
    

Next Steps