Easy📖Теория9 min

HTTP-сервер и клиент

net/http: сервер, роутинг, middleware, клиент и graceful shutdown

HTTP-сервер и клиент

Пакет net/http -- одна из жемчужин стандартной библиотеки Go. Он предоставляет production-ready HTTP-сервер и клиент без внешних зависимостей. С Go 1.22 роутер стал ещё мощнее, добавив поддержку HTTP-методов и path-параметров.

Простейший сервер

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

http.Handler -- ключевой интерфейс

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Весь HTTP в Go строится вокруг этого интерфейса. Любой тип, реализующий ServeHTTP, может обрабатывать HTTP-запросы.

// Custom handler type
type apiHandler struct {
    db *sql.DB
}

func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Handle request with access to database
    users, err := h.db.Query("SELECT name FROM users")
    if err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    defer users.Close()
    // ...
}

func main() {
    db, _ := sql.Open("postgres", "...")
    handler := &apiHandler{db: db}
    http.ListenAndServe(":8080", handler)
}

http.HandlerFunc -- адаптер

HandlerFunc -- тип-адаптер, превращающий обычную функцию в http.Handler:

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

Это позволяет использовать функции напрямую:

func healthCheck(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status":"ok"}`))
}

// Both are equivalent:
http.Handle("/health", http.HandlerFunc(healthCheck))
http.HandleFunc("/health", healthCheck) // Shortcut

http.ServeMux -- роутер

Стандартный ServeMux (до Go 1.22)

mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/users/", userHandler) // Trailing slash = subtree match

http.ListenAndServe(":8080", mux)

Улучшенный ServeMux (Go 1.22+) -- рекомендуется!

Go 1.22 добавил поддержку HTTP-методов и path-параметров прямо в стандартный роутер:

mux := http.NewServeMux()

// Method-specific routing
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

// Path parameters with {name}
mux.HandleFunc("GET /api/posts/{postID}/comments/{commentID}", getComment)

// Exact match (no subtree) with {$}
mux.HandleFunc("GET /{$}", homeHandler) // Only matches exactly "/"

// Wildcard: match rest of path
mux.HandleFunc("GET /files/{path...}", serveFiles)

http.ListenAndServe(":8080", mux)

Path-параметры (Go 1.22+)

func getUser(w http.ResponseWriter, r *http.Request) {
    // Extract path parameter
    id := r.PathValue("id")

    user, err := findUser(id)
    if err != nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func getComment(w http.ResponseWriter, r *http.Request) {
    postID := r.PathValue("postID")
    commentID := r.PathValue("commentID")

    // ...
}

func serveFiles(w http.ResponseWriter, r *http.Request) {
    // {path...} captures the rest of the URL
    filePath := r.PathValue("path") // e.g., "docs/readme.md"
    // ...
}

http.Request -- входящий запрос

func handler(w http.ResponseWriter, r *http.Request) {
    // Method
    method := r.Method // "GET", "POST", etc.

    // URL components
    path := r.URL.Path          // "/api/users"
    query := r.URL.Query()      // url.Values (map[string][]string)
    page := query.Get("page")   // "1" or ""

    // Headers
    contentType := r.Header.Get("Content-Type")
    auth := r.Header.Get("Authorization")

    // Body (for POST, PUT, PATCH)
    defer r.Body.Close()
    body, err := io.ReadAll(r.Body)

    // Context (carries deadlines, cancellation, values)
    ctx := r.Context()

    // Remote address
    remoteAddr := r.RemoteAddr // "192.168.1.1:12345"

    // Form data
    r.ParseForm()
    name := r.FormValue("name")

    // Multipart form (file uploads)
    r.ParseMultipartForm(10 << 20) // 10 MB max
    file, header, err := r.FormFile("avatar")
}

http.ResponseWriter -- формирование ответа

func handler(w http.ResponseWriter, r *http.Request) {
    // Set headers BEFORE WriteHeader or Write
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", "abc-123")
    w.Header().Set("Cache-Control", "no-cache")

    // Set status code (default 200 if not called)
    w.WriteHeader(http.StatusCreated) // 201

    // Write body
    w.Write([]byte(`{"id": "123", "name": "Alice"}`))
}

Важно: Порядок имеет значение! Заголовки нужно устанавливать до WriteHeader() или Write(). После первого Write() заголовки уже отправлены.

JSON-ответы

type ErrorResponse struct {
    Error   string `json:"error"`
    Code    int    `json:"code"`
    Details string `json:"details,omitempty"`
}

func respondJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)

    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Printf("failed to encode response: %v", err)
    }
}

func respondError(w http.ResponseWriter, status int, message string) {
    respondJSON(w, status, ErrorResponse{
        Error: message,
        Code:  status,
    })
}

Middleware-паттерн

Middleware в Go -- это функция, принимающая http.Handler и возвращающая http.Handler:

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Before handler
        // ...
        next.ServeHTTP(w, r) // Call next handler
        // After handler
        // ...
    })
}

Logging middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Wrap ResponseWriter to capture status code
        wrapped := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}

        next.ServeHTTP(wrapped, r)

        log.Printf("%s %s %d %s",
            r.Method,
            r.URL.Path,
            wrapped.statusCode,
            time.Since(start),
        )
    })
}

type statusRecorder struct {
    http.ResponseWriter
    statusCode int
}

func (r *statusRecorder) WriteHeader(code int) {
    r.statusCode = code
    r.ResponseWriter.WriteHeader(code)
}

Auth middleware

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
        }

        // Validate token
        userID, err := validateToken(strings.TrimPrefix(token, "Bearer "))
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

        // Add user ID to context
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

CORS middleware

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Recovery middleware

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("PANIC: %v\n%s", rec, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

Цепочка middleware

func chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    // Apply in reverse order so first middleware is outermost
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/users", listUsers)
    mux.HandleFunc("POST /api/users", createUser)

    // Apply middleware chain
    handler := chain(mux,
        recoveryMiddleware,
        loggingMiddleware,
        corsMiddleware,
    )

    http.ListenAndServe(":8080", handler)
}

http.Client -- HTTP-клиент

Базовое использование

// Default client (no timeout -- DON'T use in production!)
resp, err := http.Get("https://api.example.com/users")

// ALWAYS create custom client with timeout
client := &http.Client{
    Timeout: 10 * time.Second,
}

resp, err := client.Get("https://api.example.com/users")
if err != nil {
    return fmt.Errorf("fetching users: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    body, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
}

var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
    return fmt.Errorf("decoding response: %w", err)
}

Полноценный клиент с настройками

client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 10 * time.Second,
    },
}

// Custom request with headers
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
    return fmt.Errorf("creating request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", "MyApp/1.0")

resp, err := client.Do(req)
if err != nil {
    return fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()

POST-запрос с JSON

func createUser(ctx context.Context, client *http.Client, user User) (*User, error) {
    body, err := json.Marshal(user)
    if err != nil {
        return nil, fmt.Errorf("marshaling user: %w", err)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodPost,
        "https://api.example.com/users",
        bytes.NewReader(body),
    )
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("sending request: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
    }

    var created User
    if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
        return nil, fmt.Errorf("decoding response: %w", err)
    }

    return &created, nil
}

JSON API: полный пример

type UserHandler struct {
    store UserStore
}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        h.list(w, r)
    case http.MethodPost:
        h.create(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func (h *UserHandler) create(w http.ResponseWriter, r *http.Request) {
    // Limit request body size
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB

    var input struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields() // Strict parsing

    if err := dec.Decode(&input); err != nil {
        respondError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
        return
    }

    // Validate
    if input.Name == "" {
        respondError(w, http.StatusBadRequest, "Name is required")
        return
    }

    user, err := h.store.Create(r.Context(), input.Name, input.Email)
    if err != nil {
        if errors.Is(err, ErrDuplicate) {
            respondError(w, http.StatusConflict, "User already exists")
            return
        }
        log.Printf("creating user: %v", err)
        respondError(w, http.StatusInternalServerError, "Internal error")
        return
    }

    respondJSON(w, http.StatusCreated, user)
}

File Server

// Serve static files from directory
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

// Serve embedded files
//go:embed static
var staticFiles embed.FS

sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))

// Serve single file
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "static/favicon.ico")
})

Graceful Shutdown

Production-серверы должны завершаться корректно -- дождаться завершения текущих запросов:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthHandler)
    mux.HandleFunc("GET /api/users", listUsersHandler)

    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Start server in goroutine
    go func() {
        log.Printf("Server listening on %s", server.Addr)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")

    // Give outstanding requests 30 seconds to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server stopped gracefully")
}

http.MaxBytesReader -- защита от переполнения

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    // Limit to 10 MB
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20)

    if err := r.ParseMultipartForm(10 << 20); err != nil {
        var maxBytesErr *http.MaxBytesError
        if errors.As(err, &maxBytesErr) {
            respondError(w, http.StatusRequestEntityTooLarge, "File too large (max 10 MB)")
            return
        }
        respondError(w, http.StatusBadRequest, "Invalid form data")
        return
    }

    file, header, err := r.FormFile("file")
    if err != nil {
        respondError(w, http.StatusBadRequest, "Missing file")
        return
    }
    defer file.Close()

    fmt.Printf("Uploaded: %s (%d bytes)\n", header.Filename, header.Size)
}

Проверь себя

🧪

Почему нельзя использовать http.DefaultClient в production?

🧪

Как в Go 1.22+ получить path-параметр из URL вида /api/users/{id}?

🧪

Что делает server.Shutdown(ctx) при graceful shutdown?

🧪

Что такое middleware-паттерн в Go?

🧪

Какой интерфейс является центральным для HTTP-обработки в Go?