HTTP сервер из коробки

Go создавался в Google для веб-сервисов, поэтому HTTP сервер встроен в стандартную библиотеку. Никаких фреймворков не нужно для начала!

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

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Привет, %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)

    fmt.Println("Сервер запущен на :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}
http.HandleFunc регистрирует обработчик в DefaultServeMux. Каждый запрос обрабатывается в отдельной [горутине](./11-goroutines.md).

Структура Handler

http.Handler интерфейс

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

// Собственный handler
type MyHandler struct {
    greeting string
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, мир!", h.greeting)
}

func main() {
    handler := &MyHandler{greeting: "Привет"}
    http.Handle("/", handler)
    http.ListenAndServe(":8080", nil)
}

HandlerFunc - адаптер

// Превращаем функцию в Handler
func myHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello!"))
}

func main() {
    // HandlerFunc это тип, реализующий Handler
    http.Handle("/", http.HandlerFunc(myHandler))

    // Или короче
    http.HandleFunc("/", myHandler)
}

Работа с запросами

func handler(w http.ResponseWriter, r *http.Request) {
    // Метод запроса
    fmt.Printf("Method: %s\n", r.Method)

    // URL и путь
    fmt.Printf("URL: %s\n", r.URL)
    fmt.Printf("Path: %s\n", r.URL.Path)

    // Query параметры
    name := r.URL.Query().Get("name")

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

    // Cookies
    cookie, err := r.Cookie("session")
    if err == nil {
        fmt.Printf("Cookie: %s\n", cookie.Value)
    }

    // Body (для POST/PUT)
    body, err := io.ReadAll(r.Body)
    if err == nil {
        fmt.Printf("Body: %s\n", string(body))
    }
}

Работа с ответами

func handler(w http.ResponseWriter, r *http.Request) {
    // Установка заголовков (до записи тела!)
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Custom-Header", "value")

    // Установка кода ответа (до записи тела!)
    w.WriteHeader(http.StatusOK) // 200

    // Запись тела ответа
    w.Write([]byte(`{"status": "ok"}`))

    // Или используем fmt.Fprintf
    fmt.Fprintf(w, `{"message": "%s"}`, "Hello")
}
Headers и StatusCode нужно устанавливать ДО первой записи в ResponseWriter!

JSON API

JSON в Go обычно описывается структурой с тегами полей json:"...".

Отправка JSON

type Response struct {
    Status  string      `json:"status"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

func jsonHandler(w http.ResponseWriter, r *http.Request) {
    response := Response{
        Status:  "success",
        Message: "Данные получены",
        Data: map[string]interface{}{
            "users": []string{"Alice", "Bob"},
            "count": 2,
        },
    }

    w.Header().Set("Content-Type", "application/json")

    if err := json.NewEncoder(w).Encode(response); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

Получение JSON

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=0,max=150"`
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // Валидация
    if req.Name == "" || req.Email == "" {
        http.Error(w, "Name and email are required", http.StatusBadRequest)
        return
    }

    // Обработка...

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "id":      123,
        "message": "User created",
    })
}

Роутинг

Простой роутер

func router(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/":
        homeHandler(w, r)
    case "/users":
        usersHandler(w, r)
    case "/about":
        aboutHandler(w, r)
    default:
        http.NotFound(w, r)
    }
}

func main() {
    http.HandleFunc("/", router)
    http.ListenAndServe(":8080", nil)
}

RESTful роутинг

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        getUsers(w, r)
    case http.MethodPost:
        createUser(w, r)
    case http.MethodPut:
        updateUser(w, r)
    case http.MethodDelete:
        deleteUser(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

Middleware паттерн

// Middleware тип
type Middleware func(http.HandlerFunc) http.HandlerFunc

// Logging middleware
func withLogging(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    }
}

// Auth middleware
func withAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Проверка токена...

        next(w, r)
    }
}

// CORS middleware
func withCORS(next http.HandlerFunc) http.HandlerFunc {
    return 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(w, r)
    }
}

// Композиция
func main() {
    handler := withCORS(withLogging(withAuth(apiHandler)))
    http.HandleFunc("/api", handler)
    http.ListenAndServe(":8080", nil)
}

Статические файлы

func main() {
    // Обслуживание статических файлов
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    // API endpoints
    http.HandleFunc("/api/", apiHandler)

    // SPA fallback
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "./static/index.html")
    })

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

Работа с контекстом

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Добавляем значения в контекст
    ctx = context.WithValue(ctx, "userID", 123)

    // Используем контекст для отмены
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Передаем контекст дальше
    result, err := slowOperation(ctx)
    if err != nil {
        // `errors.Is` - см. урок про [обработку ошибок](./10-errors.md)
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "Request timeout", http.StatusRequestTimeout)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Write([]byte(result))
}

func slowOperation(ctx context.Context) (string, error) {
    select {
    case <-time.After(10 * time.Second):
        return "Done", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

Практический пример: REST API

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserService struct {
    mu    sync.RWMutex
    users map[int]User
    maxID int
}

func NewUserService() *UserService {
    return &UserService{
        users: make(map[int]User),
    }
}

func (s *UserService) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()

    users := make([]User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }
    return users
}

func (s *UserService) Create(user User) User {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.maxID++
    user.ID = s.maxID
    s.users[user.ID] = user

    return user
}

func (s *UserService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        users := s.GetAll()
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(users)

    case http.MethodPost:
        var user User
        if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
            http.Error(w, "Invalid JSON", http.StatusBadRequest)
            return
        }

        created := s.Create(user)
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(created)

    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    userService := NewUserService()

    mux := http.NewServeMux()
    mux.Handle("/users", userService)

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

    log.Println("Server starting on :8080")
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

Graceful shutdown

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: router(),
    }

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Ждем сигнала остановки
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
    <-quit

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

    // Graceful shutdown с таймаутом
    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 exited")
}

Итоги

  • HTTP сервер встроен в стандартную библиотеку
  • Handler интерфейс - основа всего
  • Middleware для переиспользуемой логики
  • Контекст для отмены и значений
  • Graceful shutdown для production

В следующем уроке подключим базу данных!

Типичная ошибка

Игнорировать r.Body.Close() и ошибки парсинга - потом будет «почему память течёт».

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.