HTTP-сервер на Go
HTTP-сервер на Go
Go поставляется с production-ready HTTP-сервером в стандартной библиотеке. Многие компании запускают его напрямую — без nginx, без apache, без какого-либо framework по умолчанию.
Это не игрушка. Это тот же сервер, который используется в Google внутренних инструментах.
Анатомия обработчика
Любой HTTP-обработчик в 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).
Запуск сервера
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-разработчики часто пишут один обработчик, который делает всё подряд. Правильный подход — проверять метод явно:
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
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 — это таблица маршрутов с двумя типами паттернов:
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/users | usersHandler (exact) |
/api/users/ | usersSubHandler |
/api/users/42 | usersSubHandler (subtree) |
/static/css/app.css | staticHandler (subtree) |
Паттерн / — это subtree, который матчит всё что не совпало с более конкретным. Это ловушка: если забыть зарегистрировать нужный маршрут, / поймает запрос и ты не увидишь ошибку 404.
Создание собственного ServeMux
Глобальный DefaultServeMux — плохая практика для production. Используй собственный:
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 — функция, которая оборачивает обработчик и добавляет поведение:
// Сигнатура 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 читается справа налево или изнутри наружу:
handler := Logger(Auth(CORS(mux)))
// Запрос: Logger -> Auth -> CORS -> mux -> handler
// Ответ: handler -> CORS -> Auth -> LoggerЭто паттерн "матрёшка". Каждый слой вызывает next.ServeHTTP() — или не вызывает, если хочет прервать обработку (например, Auth вернёт 401).
http.Handler vs http.HandlerFunc
Два способа создать обработчик:
// Интерфейс 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.
Заголовки и статус-коды
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 — удобный хелпер для ошибок:
http.Error(w, "not found", http.StatusNotFound)Он устанавливает Content-Type: text/plain и пишет текст с нужным кодом.
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}// нажми "Отправить запрос"