SyntaxStudy
Sign Up
Go Graceful Shutdown and HTTP Client
Go Beginner 1 min read

Graceful Shutdown and HTTP Client

A production HTTP server must handle OS signals (SIGINT, SIGTERM) to shut down gracefully. Graceful shutdown means stopping the acceptance of new connections and waiting for in-flight requests to complete before exiting. Go's `http.Server` provides a `Shutdown(ctx context.Context)` method that does exactly this. You wait for a shutdown signal in a goroutine, then call Shutdown with a context that sets the maximum wait time. The `net/http` package also includes a full-featured HTTP client. `http.Get` and `http.Post` are convenience functions for simple requests, while `http.Client` allows full control over timeouts, redirects, and transport settings. Always set a timeout on the client to prevent goroutines from hanging indefinitely on slow or unresponsive servers. HTTP client requests should use `context.Context` for cancellation and timeouts. The `http.NewRequestWithContext` function creates a request that respects the provided context. The response body must always be closed after reading — a common pattern is to defer `resp.Body.Close()` immediately after checking the error from `http.Do`.
Example
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Post struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
}

// HTTP client with timeout
func fetchPost(ctx context.Context, id int) (*Post, error) {
    url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    client := &http.Client{Timeout: 5 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("fetchPost: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("fetchPost: unexpected status %d", resp.StatusCode)
    }

    var post Post
    if err := json.NewDecoder(resp.Body).Decode(&post); err != nil {
        return nil, err
    }
    return &post, nil
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /posts/{id}", func(w http.ResponseWriter, r *http.Request) {
        post, err := fetchPost(r.Context(), 1)
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadGateway)
            return
        }
        json.NewEncoder(w).Encode(post)
    })

    srv := &http.Server{Addr: ":8080", Handler: mux}

    // Graceful shutdown
    go func() {
        quit := make(chan os.Signal, 1)
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
        <-quit
        log.Println("shutting down...")
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()
        if err := srv.Shutdown(ctx); err != nil {
            log.Fatal("forced shutdown:", err)
        }
    }()

    log.Println("listening on :8080")
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
    log.Println("server stopped cleanly")
}