Модуль
1HTTP-сервер на Go← вы здесь2JSON API3Graceful Shutdown4Финальный проект
Урок 1~12 минут

HTTP-сервер на Go

HTTP-сервер на Go

Go поставляется с production-ready HTTP-сервером в стандартной библиотеке. Многие компании запускают его напрямую — без nginx, без apache, без какого-либо framework по умолчанию.

Это не игрушка. Это тот же сервер, который используется в Google внутренних инструментах.

Анатомия обработчика

Любой HTTP-обработчик в Go — это функция с конкретной сигнатурой:

go
func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, World!")
}

Два параметра — это весь мир HTTP:

  • w http.ResponseWriter — интерфейс для записи ответа. Через него ты пишешь заголовки, статус-код и тело.
  • r *http.Request — всё о входящем запросе: метод, URL, заголовки, тело, куки.

Обрати внимание: r — это указатель (мы только читаем из него), w — интерфейс (реализация уже живёт в пакете net/http).

Запуск сервера

go
package main
 
import (
    "fmt"
    "log"
    "net/http"
)
 
func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, World!")
}
 
func main() {
    http.HandleFunc("/", hello)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

log.Fatal нужен, потому что ListenAndServe возвращает ошибку только если не смогла запуститься. В нормальной ситуации эта функция блокирует выполнение навсегда — обрабатывает запросы.

nil как второй аргумент означает использование DefaultServeMux — глобального роутера пакета.

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

Junior-разработчики часто пишут один обработчик, который делает всё подряд. Правильный подход — проверять метод явно:

go
func users(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // список пользователей
        fmt.Fprintln(w, "GET /users")
    case http.MethodPost:
        // создать пользователя
        fmt.Fprintln(w, "POST /users")
    default:
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
    }
}

http.MethodGet — это просто строковая константа "GET". Используй константы вместо строковых литералов: компилятор поймает опечатку.

Query-параметры и URL

go
func greet(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "stranger"
    }
    fmt.Fprintf(w, "Hello, %s!", name)
}
// GET /greet?name=Alice -> Hello, Alice!

r.URL.Query() возвращает url.Values — это map[string][]string. .Get() берёт первое значение или пустую строку.

Для path-параметров (типа /users/42) стандартный ServeMux не умеет. Нужно либо парсить вручную, либо взять сторонний роутер — gorilla/mux или chi.

ServeMux: как работает роутинг

http.ServeMux — это таблица маршрутов с двумя типами паттернов:

go
mux := http.NewServeMux()
mux.HandleFunc("/",            rootHandler)     // subtree: матчит всё
mux.HandleFunc("/api/users",   usersHandler)    // exact: только этот путь
mux.HandleFunc("/api/users/",  usersSubHandler) // subtree: /api/users/...
mux.HandleFunc("/static/",     staticHandler)   // subtree: /static/...

Правило: побеждает наиболее длинный совпадающий паттерн.

ЗапросОбработчик
/rootHandler
/api/usersusersHandler (exact)
/api/users/usersSubHandler
/api/users/42usersSubHandler (subtree)
/static/css/app.cssstaticHandler (subtree)

Паттерн / — это subtree, который матчит всё что не совпало с более конкретным. Это ловушка: если забыть зарегистрировать нужный маршрут, / поймает запрос и ты не увидишь ошибку 404.

Создание собственного ServeMux

Глобальный DefaultServeMux — плохая практика для production. Используй собственный:

go
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", healthHandler)
    mux.HandleFunc("/api/", apiHandler)
 
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
 
    log.Fatal(server.ListenAndServe())
}

Таймауты — обязательны. Без них один медленный клиент может держать горутину вечно.

Middleware

Middleware — функция, которая оборачивает обработчик и добавляет поведение:

go
// Сигнатура middleware
type Middleware func(http.Handler) http.Handler
 
func Logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}
 
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", usersHandler)
 
    // Оборачиваем: каждый запрос пройдёт через Logger
    handler := Logger(mux)
    http.ListenAndServe(":8080", handler)
}

Цепочка middleware читается справа налево или изнутри наружу:

go
handler := Logger(Auth(CORS(mux)))
// Запрос: Logger -> Auth -> CORS -> mux -> handler
// Ответ:  handler -> CORS -> Auth -> Logger

Это паттерн "матрёшка". Каждый слой вызывает next.ServeHTTP() — или не вызывает, если хочет прервать обработку (например, Auth вернёт 401).

http.Handler vs http.HandlerFunc

Два способа создать обработчик:

go
// Интерфейс http.Handler: любой тип с методом ServeHTTP
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
 
// Адаптер http.HandlerFunc: конвертирует обычную функцию в http.Handler
type HandlerFunc func(ResponseWriter, *Request)
 
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

http.HandlerFunc — это type conversion, не вызов функции. Она позволяет использовать обычные функции там, где ожидается интерфейс http.Handler.

Заголовки и статус-коды

go
func handler(w http.ResponseWriter, r *http.Request) {
    // Заголовки нужно ставить ДО вызова WriteHeader или Write
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", "abc123")
 
    // Явный статус-код — тоже до тела
    w.WriteHeader(http.StatusCreated) // 201
 
    // Тело
    fmt.Fprintln(w, `{"status":"created"}`)
}

Важный порядок: заголовки → WriteHeader → тело. Если написать тело первым, Go автоматически вызовет WriteHeader(200) и больше изменить статус нельзя — заголовки уже ушли клиенту.

http.Error — удобный хелпер для ошибок:

go
http.Error(w, "not found", http.StatusNotFound)

Он устанавливает Content-Type: text/plain и пишет текст с нужным кодом.

net/http в стандартной библиотеке Go — один из лучших HTTP-пакетов в экосистеме. Nginx и Apache не нужны — Go сам является веб-сервером.
Поток запроса
Browser
GET /hello
handler()
w, r
Response
200 OK
handler.go
func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, World!")
}
w — куда писать ответ, r — входящий запрос
HTTP Response
// нажми "Отправить запрос"
🎯
Миссия 1 из 4
Какие два параметра принимает HTTP-обработчик в Go?