DEV Community

Mofajjal Rasul
Mofajjal Rasul

Posted on

HTTP Server Management in Go: Graceful Shutdowns and Error Handling

Running a web server isn’t just about handling requests—it's about lifecycle control. You want predictable startup, clear error handling, and graceful shutdown.

func main() {
    // 1. Initialize server
    srv := webapi.NewServer() // Preconfigured *http.Server

    // 2. Error channel (buffer size 1 prevents leaks)
    serverErrCh := make(chan error, 1)

    // 3. Start server in goroutine
    go func() {
        log.Printf("Starting server on http://%s ...", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            serverErrCh <- err  // Report startup errors
        }
    }()

    // 4. Shutdown signal listener
    shutdownCh := make(chan os.Signal, 1)
    signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) // Capture Ctrl+C/kill

    // 5. Wait for either error or shutdown signal
    select {
    case err := <-serverErrCh:
        log.Fatalf("Server error: %v", err) // Startup failed

    case <-shutdownCh:
        log.Println("Shutting down server...")
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel() // Always release context resources!

        // 6. Attempt graceful shutdown
        if err := srv.Shutdown(ctx); err != nil {
            log.Printf("Graceful shutdown failed: %v", err)
            // 7. Force close if graceful fails
            if err = srv.Close(); err != nil {
                log.Fatalf("Forced shutdown error: %v", err)
            }
        }
        log.Println("Server exited gracefully")
    }
}
Enter fullscreen mode Exit fullscreen mode

What This Code Really Does

1. Server Initialization

srv := webapi.NewServer()
Enter fullscreen mode Exit fullscreen mode

This creates a preconfigured HTTP server—potentially with address, routes, and timeouts already set. It keeps the code clean and reusable.

2. Error Channel for Server Runtime

serverErrCh := make(chan error, 1)
Enter fullscreen mode Exit fullscreen mode

We set up a buffered error channel to listen for startup or runtime issues. The buffer of 1 ensures no goroutines get stuck if an error isn’t read immediately.

3. Starting the Server Concurrently

go func() {
    log.Printf("Starting server on http://%s ...", srv.Addr)
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        serverErrCh <- err
    }
}()
Enter fullscreen mode Exit fullscreen mode

By starting the server in a goroutine, we avoid blocking the main execution. It listens and serves, and if something goes wrong, we send the error to the channel—unless it's an intentional shutdown.

4. Listening for OS Signals

shutdownCh := make(chan os.Signal, 1)
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
Enter fullscreen mode Exit fullscreen mode

This sets up a signal listener to catch system interrupts (like Ctrl+C or a Docker stop event). This is key for real-world server deployments that need to shut down gracefully.

5. Coordinating Shutdown or Errors

select {
case err := <-serverErrCh:
    log.Fatalf("Server error: %v", err)
case <-shutdownCh:
    log.Println("Shutting down server...")
    ...
}
Enter fullscreen mode Exit fullscreen mode

We wait for either a server error or an external shutdown signal. Whichever happens first dictates the next step.

6. Graceful Shutdown with Timeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Enter fullscreen mode Exit fullscreen mode

This timeout context gives the server up to 5 seconds to shut down cleanly. Any ongoing requests are allowed to finish—ideal for preventing dropped traffic.

7. Fallback to Forced Shutdown

if err := srv.Shutdown(ctx); err != nil {
    ...
    if err = srv.Close(); err != nil {
        log.Fatalf("Forced shutdown error: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

If graceful shutdown fails (for example, due to a stuck connection), the server is forcefully closed to ensure the process exits and frees resources.


Why This Pattern Matters

This isn’t just clean code—it’s operationally sound. It’s what you need when deploying Go services behind a load balancer, inside containers, or on bare metal:

  • Startup and error visibility
  • Responsiveness to OS signals
  • Clean exit path with fallbacks

It’s a tested way to make your Go services production-ready—resilient, observable, and predictable.

Top comments (0)