net/http: HTTP-сервер и клиент в Go
Кратко:
net/http— стандартный пакет для HTTP-сервера и клиента в Go. Никаких фреймворков не нужно — большинство сервисов в проде написаны на голом stdlib (или тонкой обёртке). Понимание net/http — фундамент любого Go-бэкендера.Зачем знать джуну: На собесе спросят “как написать HTTP-сервер без фреймворка?”, “что не так с http.Get в проде?”, “как сделать graceful shutdown?”. В production к тебе придут с зависшим сервисом — окажется, нет таймаута на клиенте, или body не закрыт, или handler паникует.
Содержание
Заголовок раздела «Содержание»- Базовое API: Server и Client
- Под капотом: lifecycle запроса, горутина на запрос
- Gotchas: типичные ловушки
- Производительность: пулы соединений, профилирование
- Вопросы на собесе
- Practice
- Источники
1. Базовое API
Заголовок раздела «1. Базовое API»1.1 Минимальный HTTP-сервер
Заголовок раздела «1.1 Минимальный HTTP-сервер»package main
import ( "fmt" "log" "net/http")
func main() { http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!\n", r.URL.Query().Get("name")) })
log.Fatal(http.ListenAndServe(":8080", nil))}Запусти, открой http://localhost:8080/hello?name=world — увидишь “Hello, world!”.
⚠️ Этот код НЕ для production. Нет таймаутов, используется глобальный DefaultServeMux, нет graceful shutdown.
1.2 http.Handler interface
Заголовок раздела «1.2 http.Handler interface»Сердце всей системы:
type Handler interface { ServeHTTP(w http.ResponseWriter, r *http.Request)}Любой объект, реализующий ServeHTTP, может быть HTTP-хендлером:
type myHandler struct { greeting string}
func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s, %s!\n", h.greeting, r.URL.Query().Get("name"))}
func main() { h := &myHandler{greeting: "Привет"} http.ListenAndServe(":8080", h)}1.3 http.HandlerFunc — адаптер
Заголовок раздела «1.3 http.HandlerFunc — адаптер»type HandlerFunc func(ResponseWriter, *Request)
// HandlerFunc реализует Handler через вызов f(w, r)func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r)}Это позволяет передавать обычную функцию туда, где ждут Handler:
func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello")}
// Преобразуем функцию в Handler:var h http.Handler = http.HandlerFunc(hello)http.HandleFunc под капотом делает именно это.
1.4 http.ServeMux — роутер
Заголовок раздела «1.4 http.ServeMux — роутер»ServeMux — простой мультиплексор: по path выбирает Handler.
mux := http.NewServeMux()
mux.HandleFunc("/users", listUsers)mux.HandleFunc("/users/", getUser) // обрати внимание на trailing slash!
http.ListenAndServe(":8080", mux)До Go 1.22 ServeMux был очень простой: только path, без поддержки методов и path-параметров.
1.5 Enhanced ServeMux в Go 1.22+
Заголовок раздела «1.5 Enhanced ServeMux в Go 1.22+»С Go 1.22 ServeMux поддерживает методы и wildcards. Это убрало необходимость в фреймворках для большинства задач!
mux := http.NewServeMux()
// Method + pathmux.HandleFunc("GET /users", listUsers)mux.HandleFunc("POST /users", createUser)mux.HandleFunc("GET /users/{id}", getUser)mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Wildcard в конце путиmux.HandleFunc("GET /files/{path...}", serveFile)Получить параметр пути:
func getUser(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") fmt.Fprintf(w, "User ID: %s\n", id)}⚠️ Подвох: если паттерн пересекается с другим без указания метода (/users/{id} vs GET /users/{id}), будет паника при регистрации. Используй методы во всех роутах для ясности.
1.6 Production-ready Server с таймаутами
Заголовок раздела «1.6 Production-ready Server с таймаутами»Обязательная конфигурация:
srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, // защита от Slowloris ReadTimeout: 10 * time.Second, // лимит на полное чтение запроса WriteTimeout: 10 * time.Second, // лимит на запись ответа IdleTimeout: 60 * time.Second, // keep-alive MaxHeaderBytes: 1 << 20, // 1 MB}
log.Fatal(srv.ListenAndServe())Зачем каждый таймаут:
| Таймаут | Защищает от |
|---|---|
ReadHeaderTimeout | Slowloris атака (медленная отправка заголовков) |
ReadTimeout | Медленный/застрявший клиент (включая body) |
WriteTimeout | Медленная запись ответа |
IdleTimeout | Висящие keep-alive соединения |
⚠️ БЕЗ таймаутов сервер уязвим. Это самая частая ошибка джунов.
1.7 Чтение Request
Заголовок раздела «1.7 Чтение Request»func handler(w http.ResponseWriter, r *http.Request) { // Method fmt.Println(r.Method) // GET, POST, etc.
// URL fmt.Println(r.URL.Path) // /users/123 fmt.Println(r.URL.RawQuery) // foo=bar fmt.Println(r.URL.Query().Get("foo")) // bar
// Headers fmt.Println(r.Header.Get("Content-Type")) fmt.Println(r.Header.Get("User-Agent"))
// Body defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } fmt.Println(string(body))
// Context (Go 1.7+) ctx := r.Context() // ctx отменится, если клиент разорвёт соединение}1.8 Form Parsing
Заголовок раздела «1.8 Form Parsing»func handler(w http.ResponseWriter, r *http.Request) { // Парсит query + body для POST с Content-Type: application/x-www-form-urlencoded if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return }
name := r.FormValue("name") // query или form only := r.PostFormValue("name") // только form (POST body) fmt.Println(name, only)
// r.Form — все, r.PostForm — только из body}Для multipart/form-data (file upload) — r.ParseMultipartForm(maxMemory).
1.9 ResponseWriter
Заголовок раздела «1.9 ResponseWriter»func handler(w http.ResponseWriter, r *http.Request) { // Установить headers (ДО WriteHeader/Write!) w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Custom", "value")
// Установить status code (опционально, дефолт 200) w.WriteHeader(http.StatusCreated)
// Записать body fmt.Fprintf(w, `{"id": 42}`)}⚠️ КРИТИЧНО: w.Header().Set() работает только до первого Write() или WriteHeader(). После — игнорируется!
// Это БАГ:w.Write([]byte("data"))w.Header().Set("Content-Type", "application/json") // ИГНОРИРУЕТСЯ1.10 Middleware — концепт
Заголовок раздела «1.10 Middleware — концепт»Middleware — функция, оборачивающая Handler:
func loggingMiddleware(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 authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "secret" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) })}
func main() { mux := http.NewServeMux() mux.HandleFunc("GET /api/data", getData)
// Цепочка: log → auth → handler handler := loggingMiddleware(authMiddleware(mux))
srv := &http.Server{Addr: ":8080", Handler: handler} srv.ListenAndServe()}1.11 HTTP Client — минимум
Заголовок раздела «1.11 HTTP Client — минимум»resp, err := http.Get("https://api.example.com/users")if err != nil { log.Fatal(err)}defer resp.Body.Close() // КРИТИЧНО!
body, _ := io.ReadAll(resp.Body)fmt.Println(string(body))⚠️ ОПАСНО в production: http.Get использует http.DefaultClient, у которого нет таймаута. Зависший сервер → твоя горутина висит вечно → leak.
1.12 Production-ready Client
Заголовок раздела «1.12 Production-ready Client»client := &http.Client{ Timeout: 10 * time.Second, // общий таймаут на весь запрос Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 5 * time.Second, ExpectContinueTimeout: 1 * time.Second, },}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)if err != nil { return err}req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)if err != nil { return err}defer resp.Body.Close()Best practice: один *http.Client на весь сервис (singleton). Transport кеширует TCP-соединения (connection pool).
1.13 Graceful shutdown
Заголовок раздела «1.13 Graceful shutdown»srv := &http.Server{Addr: ":8080", Handler: mux}
go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server: %v", err) }}()
// Ждём сигналsigCh := make(chan os.Signal, 1)signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)<-sigCh
log.Println("shutting down...")ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()
if err := srv.Shutdown(ctx); err != nil { log.Printf("shutdown error: %v", err)}log.Println("server stopped")Shutdown перестаёт принимать новые соединения и ждёт завершения активных запросов. Если за ctx дедлайн не успели — закрывает принудительно.
2. Под капотом
Заголовок раздела «2. Под капотом»2.1 Lifecycle запроса
Заголовок раздела «2.1 Lifecycle запроса»Клиент Server │ │ │── TCP connect ──→│ (Listener accepts) │ │ │ │── go conn.serve() (горутина на соединение!) │ │ │ │── Request ──────→│ │── readRequest() │ │ │── ServeMux.Handler(r) │ │ │── Handler.ServeHTTP(w, r) │ │ │── flush response │←── Response ─────│ │ │ │ │── (keep-alive: ждём след. запрос) │ │ │ или conn.close()Ключевая идея: каждое соединение обслуживается в отдельной горутине. Это даёт высокую конкурентность из коробки.
В рамках одного keep-alive соединения может быть несколько запросов — все обрабатываются последовательно в одной горутине (HTTP/1.1). С HTTP/2 — мультиплексирование.
2.2 Каждый запрос — своя горутина
Заголовок раздела «2.2 Каждый запрос — своя горутина»func handler(w http.ResponseWriter, r *http.Request) { // Этот код выполняется в горутине, созданной runtime'ом net/http. // 1000 одновременных запросов = ~1000 горутин. // Это нормально — горутины дешёвые.}⚠️ Важно: общая память между горутинами требует синхронизации (мьютексов, channels, atomic). Если ты хранишь shared state в handler без блокировки — race condition.
// БАГ:var counter intfunc handler(w http.ResponseWriter, r *http.Request) { counter++ // race!}
// Правильно:var counter atomic.Int64func handler(w http.ResponseWriter, r *http.Request) { counter.Add(1)}2.3 ServeMux — что внутри
Заголовок раздела «2.3 ServeMux — что внутри»ServeMux хранит map: pattern → handler. При запросе ищет longest match по path. С Go 1.22 добавилась поддержка методов и path-параметров через более сложный матчер.
// Упрощённо:type ServeMux struct { mu sync.RWMutex tree *routingNode // для Go 1.22+ patterns []*pattern}2.4 Default mux — это global state
Заголовок раздела «2.4 Default mux — это global state»http.HandleFunc("/foo", handler) // регистрирует в http.DefaultServeMuxhttp.ListenAndServe(":8080", nil) // nil = DefaultServeMux⚠️ Подвох: DefaultServeMux — глобальная переменная. Если в твою программу импортируется пакет (например, net/http/pprof), он автоматически регистрирует обработчики в DefaultServeMux. В production это может неожиданно экспонировать debug endpoints!
Best practice: всегда явно создавай свой mux := http.NewServeMux().
2.5 ResponseWriter — что это на самом деле
Заголовок раздела «2.5 ResponseWriter — что это на самом деле»ResponseWriter — интерфейс. Конкретная реализация (http.response) пишет в TCP-соединение. Внутри есть буфер (bufio.Writer). Первый Write или явный WriteHeader “коммитит” статус и заголовки — они уходят в сокет, и дальше менять нельзя.
type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int)}2.6 Hijacker — взять под контроль соединение
Заголовок раздела «2.6 Hijacker — взять под контроль соединение»Для WebSocket, raw TCP внутри HTTP:
hj, ok := w.(http.Hijacker)if !ok { http.Error(w, "not hijackable", 500) return}conn, bufrw, err := hj.Hijack()// теперь conn — твоё, дальше пиши/читай напрямую2.7 HTTP/2 в Go
Заголовок раздела «2.7 HTTP/2 в Go»С Go 1.6+ HTTP/2 включён по умолчанию для HTTPS. Никаких настроек не надо:
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)// HTTP/2 поддерживается автоматическиДля HTTP/2 без TLS (h2c) — нужно явное включение через golang.org/x/net/http2/h2c.
2.8 Connection pool в Transport
Заголовок раздела «2.8 Connection pool в Transport»transport := &http.Transport{ MaxIdleConns: 100, // всего idle MaxIdleConnsPerHost: 10, // idle на хост IdleConnTimeout: 90 * time.Second,}После запроса соединение возвращается в пул и переиспользуется. Это критично для производительности — TCP+TLS handshake очень дорогой.
⚠️ Если не вызвать resp.Body.Close() — соединение не возвращается в пул. Каждый запрос будет открывать новое TCP-соединение.
2.9 WebSocket (через сторонние библиотеки)
Заголовок раздела «2.9 WebSocket (через сторонние библиотеки)»net/http не поддерживает WebSocket напрямую. Используются:
github.com/gorilla/websocket(популярный, проверенный).nhooyr.io/websocket(более современный).
import "github.com/gorilla/websocket"
var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024,}
func wsHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close()
for { msgType, msg, err := conn.ReadMessage() if err != nil { return } conn.WriteMessage(msgType, msg) }}2.10 TLS базово
Заголовок раздела «2.10 TLS базово»err := http.ListenAndServeTLS(":443", "cert.pem", // публичный сертификат "key.pem", // приватный ключ mux)Для production используют Let’s Encrypt через golang.org/x/crypto/acme/autocert:
m := &autocert.Manager{ Cache: autocert.DirCache("certs"), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist("example.com"),}srv := &http.Server{ Addr: ":443", TLSConfig: m.TLSConfig(),}go http.ListenAndServe(":80", m.HTTPHandler(nil)) // ACME challengesrv.ListenAndServeTLS("", "")3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ 3.1 http.Get/Post без таймаута
Заголовок раздела «⚠️ 3.1 http.Get/Post без таймаута»// БАГ:resp, err := http.Get("http://slow-server.com")
// http.DefaultClient.Timeout == 0 (нет таймаута)// Зависший сервер → зависнет твоя горутина → утечкаРешение: свой *http.Client с Timeout.
⚠️ 3.2 Не закрытый Body
Заголовок раздела «⚠️ 3.2 Не закрытый Body»resp, err := client.Do(req)if err != nil { return err}// ЗАБЫЛИ defer resp.Body.Close()// Соединение не возвращается в пул, TCP не закрывается.Правило: всегда defer resp.Body.Close() после успешной проверки err.
if err != nil { return err}defer resp.Body.Close()// ВАЖНО: даже если ты не читаешь body — закрой!⚠️ 3.3 Не дочитанный Body
Заголовок раздела «⚠️ 3.3 Не дочитанный Body»resp, err := client.Do(req)if err != nil { return err }defer resp.Body.Close()
if resp.StatusCode != 200 { return errors.New("bad status") // body не дочитан → keep-alive не используется → новое соединение для следующего запроса!}io.ReadAll(resp.Body)Правило: дочитай body до конца, даже если статус не 200:
defer func() { io.Copy(io.Discard, resp.Body) resp.Body.Close()}()⚠️ 3.4 Установка Header после Write
Заголовок раздела «⚠️ 3.4 Установка Header после Write»func handler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) w.Header().Set("Content-Type", "text/plain") // ИГНОРИРУЕТСЯ}Правило: Header → WriteHeader → Write. В этом порядке.
⚠️ 3.5 Panic в handler — что будет?
Заголовок раздела «⚠️ 3.5 Panic в handler — что будет?»net/http ловит panic в handler через defer recover(). Соединение закроется, но программа не упадёт. Однако:
- Лог покажет panic (через
Server.ErrorLog). - Клиент получит обрезанный ответ или ничего.
- Это плохо! Лучше использовать middleware-recovery с логированием в твой logger.
func recovery(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 error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) })}⚠️ 3.6 Shared state без mutex
Заголовок раздела «⚠️ 3.6 Shared state без mutex»var requests = make(map[string]int)
func handler(w http.ResponseWriter, r *http.Request) { requests[r.URL.Path]++ // race! map не concurrent-safe}Решение: sync.Mutex, sync.Map, или atomic для счётчиков.
⚠️ 3.7 Утечка соединений из-за goroutine leak
Заголовок раздела «⚠️ 3.7 Утечка соединений из-за goroutine leak»func handler(w http.ResponseWriter, r *http.Request) { go func() { // длинная работа без context time.Sleep(10 * time.Minute) }() w.Write([]byte("ok"))}Клиент уже ушёл, но горутина в фоне работает. Если их тысячи — память течёт. Используй context:
func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() go doWork(ctx) // работа отменится при разрыве соединения}⚠️ Но r.Context() отменяется и когда handler возвращается! Если работа должна продолжаться — используй context.Background() с собственным таймаутом.
⚠️ 3.8 Кэширование DNS
Заголовок раздела «⚠️ 3.8 Кэширование DNS»http.Transport не кеширует DNS долго (использует системный resolver). Если DNS меняется (rolling deployment в k8s), Transport может держать старые IP. Решение — короткий IdleConnTimeout + создание новых соединений периодически.
⚠️ 3.9 ServeMux pattern с /
Заголовок раздела «⚠️ 3.9 ServeMux pattern с /»mux.HandleFunc("/", root) // ловит ВСЕ пути (prefix match)mux.HandleFunc("/api/v1", api) // только точное совпадение
// В Go 1.22+ можно явно: "/{$}" — только кореньmux.HandleFunc("/{$}", root)⚠️ 3.10 r.Body уже прочитан middleware
Заголовок раздела «⚠️ 3.10 r.Body уже прочитан middleware»// Middleware:body, _ := io.ReadAll(r.Body)log.Println(string(body))// r.Body теперь пустой!
next.ServeHTTP(w, r)// Handler пытается прочитать body → ничегоРешение:
body, _ := io.ReadAll(r.Body)r.Body = io.NopCloser(bytes.NewReader(body))⚠️ 3.11 Bare ListenAndServe без recover/log
Заголовок раздела «⚠️ 3.11 Bare ListenAndServe без recover/log»log.Fatal(http.ListenAndServe(":8080", nil))// при ошибке log.Fatal → os.Exit(1), defer'ы НЕ выполнятся!Используй Server.Shutdown для graceful, а ошибку логгируй явно.
⚠️ 3.12 Content-Length и Transfer-Encoding
Заголовок раздела «⚠️ 3.12 Content-Length и Transfer-Encoding»Если ты вызываешь w.Write(data) один раз, Go автоматически выставит Content-Length. Если несколько раз и не знаешь общую длину — будет Transfer-Encoding: chunked. Это нормально, но имей в виду.
4. Производительность
Заголовок раздела «4. Производительность»4.1 Connection pool — самый большой выигрыш
Заголовок раздела «4.1 Connection pool — самый большой выигрыш»Правило: один *http.Client на сервис. Он переиспользует TCP+TLS соединения.
// БАГ — каждый запрос новый client → новый pool → новые соединенияfunc get(url string) { client := &http.Client{Timeout: 10 * time.Second} resp, _ := client.Get(url) defer resp.Body.Close() // ...}
// OK — один client на всю программуvar client = &http.Client{Timeout: 10 * time.Second}4.2 MaxIdleConnsPerHost
Заголовок раздела «4.2 MaxIdleConnsPerHost»Дефолт 2. Если ходишь много в один сервис — увеличь:
transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 20, // важно! IdleConnTimeout: 90 * time.Second,}4.3 HTTP/2 multiplexing
Заголовок раздела «4.3 HTTP/2 multiplexing»С HTTP/2 одно соединение обрабатывает много параллельных запросов. Это часто лучше, чем много HTTP/1.1 соединений.
4.4 Снижаем аллокации в handler
Заголовок раздела «4.4 Снижаем аллокации в handler»// БАГ — fmt.Fprintf аллоцируетfmt.Fprintf(w, "user: %d\n", id)
// OK — буферизованный writer + strconvbuf := bufPool.Get().(*bytes.Buffer)buf.Reset()buf.WriteString("user: ")buf.WriteString(strconv.Itoa(id))buf.WriteByte('\n')w.Write(buf.Bytes())bufPool.Put(buf)В большинстве случаев это не нужно. Но в hot path (десятки тысяч RPS) — оправдано.
4.5 net/http/pprof — профилирование HTTP-сервера
Заголовок раздела «4.5 net/http/pprof — профилирование HTTP-сервера»import _ "net/http/pprof"
func main() { go http.ListenAndServe("localhost:6060", nil) // pprof endpoints // ... твой основной сервер на :8080}Эндпоинты:
/debug/pprof/profile?seconds=30— CPU profile./debug/pprof/heap— heap./debug/pprof/goroutine— все горутины./debug/pprof/trace?seconds=5— execution trace.
⚠️ Не выставляй pprof в публичный интернет! Только на внутренний порт.
4.6 wrk/hey для нагрузочного тестирования
Заголовок раздела «4.6 wrk/hey для нагрузочного тестирования»# hey: 100 одновременных соединений, 10 секhey -c 100 -z 10s http://localhost:8080/
# wrk: 4 потока, 100 connections, 30 секwrk -t4 -c100 -d30s http://localhost:8080/Метрики смотрим:
- RPS (requests per second).
- p50/p95/p99 latency.
- Errors.
4.7 Бенчмарк handler через httptest
Заголовок раздела «4.7 Бенчмарк handler через httptest»func BenchmarkHandler(b *testing.B) { req := httptest.NewRequest("GET", "/users/42", nil) h := http.HandlerFunc(getUser)
b.ResetTimer() for i := 0; i < b.N; i++ { w := httptest.NewRecorder() h.ServeHTTP(w, req) }}go test -bench=. -benchmem5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»Базовые
Заголовок раздела «Базовые»1. Что такое http.Handler?
Интерфейс с методом ServeHTTP(w ResponseWriter, r *Request). Любой тип, реализующий его, может обслуживать HTTP-запросы.
2. Что такое http.HandlerFunc? Адаптер: тип-функция, у которой есть метод ServeHTTP, который просто вызывает саму функцию. Позволяет передавать обычные функции туда, где ждут Handler.
3. Что такое ServeMux? Простой роутер: маппит path (с Go 1.22 — и метод + параметры) на Handler.
4. Что нового в ServeMux Go 1.22?
Поддержка методов (GET /users), path-параметров (/users/{id}), wildcards (/files/{path...}), и более строгое сопоставление.
5. Что не так с http.Get в production?
Использует http.DefaultClient без таймаута. Зависший сервер → горутина висит вечно.
6. Какие таймауты нужны на http.Server? ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout. Каждый защищает от своего класса проблем (Slowloris, slow body, slow write, висящие keep-alive).
7. Как сделать graceful shutdown?
Запускаем сервер в горутине, ловим SIGTERM/SIGINT, вызываем srv.Shutdown(ctx) — он ждёт активные запросы, не принимает новые.
8. Сколько горутин использует net/http? Одна на TCP-соединение (для HTTP/1.1). HTTP/2 — одна на stream. 1000 одновременных соединений = ~1000 горутин.
9. Почему важен resp.Body.Close()? Без закрытия TCP-соединение не возвращается в пул → утечка соединений, потеря производительности.
10. Что такое middleware? Функция, оборачивающая Handler: принимает Handler, возвращает Handler. Позволяет добавить логирование, аутентификацию, recovery и т.п. сквозным образом.
Средние
Заголовок раздела «Средние»11. ResponseWriter порядок вызовов?
Header().Set(...)— установить headers.WriteHeader(status)— отправить статус и headers.Write(data)— отправить body. После Write/WriteHeader заголовки уже отправлены — менять нельзя.
12. Что такое DefaultServeMux и почему его опасно использовать? Глобальная переменная. Другие пакеты могут регистрировать в неё handlers (например, net/http/pprof). Это может неожиданно выставить debug endpoints.
13. Как обработать panic в handler? net/http сам ловит панику и закрывает соединение. Но лучше использовать recovery middleware: логгировать stack trace и возвращать 500.
14. HTTP/2 — нужно ли что-то делать? В Go HTTP/2 включён by default для HTTPS. Для h2c (без TLS) — отдельная библиотека.
15. Что делает Transport.MaxIdleConnsPerHost? Лимит idle (keep-alive) соединений к одному хосту в пуле. Дефолт 2 — часто мало. Для микросервисов поднимают до 50-100.
16. Как передать данные из middleware в handler?
Через r.Context():
ctx := context.WithValue(r.Context(), "userID", id)next.ServeHTTP(w, r.WithContext(ctx))17. r.Body — зачем дочитывать до конца? Чтобы keep-alive соединение могло переиспользоваться. Иначе сервер не знает, где конец запроса, и закроет соединение.
18. Чем отличается r.Form от r.PostForm? r.Form — всё (query + body), r.PostForm — только из body (только для POST с form-encoded).
Продвинутые
Заголовок раздела «Продвинутые»19. Что такое Hijacker?
Интерфейс, позволяющий взять TCP-соединение под прямой контроль (для WebSocket, custom протоколов). Получаем raw net.Conn.
20. Как ограничить размер body?
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB21. Что такое keep-alive и как им управлять?
TCP-соединение остаётся открытым после ответа, чтобы переиспользоваться для следующих запросов. Управляется заголовком Connection: keep-alive (дефолт в HTTP/1.1). Server отключает: Server.SetKeepAlivesEnabled(false).
22. Как сделать SSE (Server-Sent Events)?
Установить Content-Type: text/event-stream, использовать http.Flusher:
flusher := w.(http.Flusher)fmt.Fprintf(w, "data: hello\n\n")flusher.Flush()23. Как сделать proxying запросов?
net/http/httputil.ReverseProxy:
proxy := httputil.NewSingleHostReverseProxy(targetURL)http.Handle("/", proxy)24. Что произойдёт при разрыве соединения клиентом во время handler?
r.Context() отменится. Если handler не проверяет ctx — продолжит работать (а WriteResponse уйдёт в никуда). Лучшая практика — респектить ctx.Done() в длинных операциях.
25. Чем gin/echo лучше net/http? Удобнее API (особенно для path params до Go 1.22), сложные middleware-цепочки, validation, binding. Но Go 1.22 enhanced ServeMux закрывает большую часть use cases. Для микросервисов часто хватает stdlib.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Production-ready сервер
Заголовок раздела «Задача 1: Production-ready сервер»Напиши HTTP-сервер с:
- эндпоинт
GET /users/{id}, - middleware: логирование, recovery, request ID,
- таймауты,
- graceful shutdown по SIGTERM,
/healthzдля k8s probe.
Задача 2: HTTP-клиент с retry
Заголовок раздела «Задача 2: HTTP-клиент с retry»Напиши функцию Get(url string) ([]byte, error), которая:
- использует общий
*http.Clientс таймаутом, - ретраит до 3 раз при сетевых ошибках,
- exponential backoff (1s, 2s, 4s),
- уважает context,
- правильно закрывает body.
Задача 3: Reverse proxy
Заголовок раздела «Задача 3: Reverse proxy»Напиши прокси, который:
- принимает запросы на
:8080, - форвардит на
http://localhost:9000, - добавляет header
X-Forwarded-For, - логгирует время каждого запроса.
Используй httputil.ReverseProxy.
Задача 4: Найди утечку
Заголовок раздела «Задача 4: Найди утечку»func handler(w http.ResponseWriter, r *http.Request) { resp, err := http.Get("https://api.example.com/data") if err != nil { http.Error(w, err.Error(), 500) return } body, _ := io.ReadAll(resp.Body) w.Write(body)}Найди две проблемы. (Подсказка: timeout, body.Close).
Задача 5: Сравни производительность
Заголовок раздела «Задача 5: Сравни производительность»Напиши бенчмарк, сравнивающий:
http.HandleFunc+ ручной парсинг pattern/users/{id}черезstrings.Split,- Go 1.22 enhanced ServeMux с
r.PathValue("id").
Сравни B/op и ns/op.
7. Источники
Заголовок раздела «7. Источники»- Документация net/http — https://pkg.go.dev/net/http
- Enhanced ServeMux Go 1.22 — https://go.dev/blog/routing-enhancements
- The complete guide to Go net/http timeouts — https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
- gorilla/websocket — https://github.com/gorilla/websocket
- Server Programming with Go (книга) — Mat Ryer, Go Programming Blueprints.
- 100 Go Mistakes — Teiva Harsanyi (глава 11 — HTTP).
- Go Web Examples — https://gowebexamples.com/
Чек-лист джуна:
- Могу написать HTTP-сервер без фреймворка.
- Знаю, какие 4 таймаута нужны на http.Server.
- Никогда не использую http.Get в проде.
- Всегда
defer resp.Body.Close()+ дочитываю body. - Делаю graceful shutdown через
srv.Shutdown(ctx). - Понимаю, что middleware — это обёртка handler.
- Знаю про Go 1.22 enhanced ServeMux.
- Использую
r.Context()в длинных операциях. - Знаю, как профилировать через net/http/pprof.