Лекция 3. HTTP-клиент и HTTP-сервер на стандартной библиотеке¶
В теме 10 лекции 1 мы разбирали urllib и requests для Python и параллельно немного коснулись net/http в Go. Здесь — расширенный разбор: production-ready клиент, маршрутизация на Go 1.22+, middleware, graceful shutdown.
HTTP-клиент¶
Простейший GET¶
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.github.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode)
fmt.Println(string(body))
}
Ключевые вещи:
resp.Body.Close()черезdefer— обязательно. Иначе TCP-соединение не вернётся в пул.resp.Body— этоio.ReadCloser. Стандартные читатели (io.ReadAll,bufio.NewScanner,json.NewDecoder) с ним работают.
Не используйте http.DefaultClient в продакшене¶
http.Get, http.Post, http.DefaultClient — это глобальные клиенты без таймаутов. Если сервер не отвечает — горутина будет висеть вечно. В обучающем коде сойдёт, но не для прода.
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
http.Client.Timeout — суммарный таймаут на запрос (включая dial, TLS, чтение тела). Для более тонкого контроля — настройки внутри Transport.
Запрос с заголовками и контекстом¶
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("post %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("status %d: %s", resp.StatusCode, body)
}
Любой клиентский вызов в библиотечной функции должен принимать ctx context.Context и передавать его в NewRequestWithContext. Это даёт отмену при таймауте/отмене запроса вызывающего.
Парсинг JSON-ответа¶
type GitHubUser struct {
Login string `json:"login"`
Name string `json:"name"`
Bio string `json:"bio"`
}
resp, err := client.Get("https://api.github.com/users/jtprogru")
// ...
var u GitHubUser
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
return err
}
json.NewDecoder(resp.Body).Decode лучше, чем ReadAll + Unmarshal: не нужно держать всё тело в памяти. Подробно про JSON — следующая лекция.
HTTP-сервер¶
Минимальный сервер¶
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Привет!")
})
http.ListenAndServe(":8080", nil)
}
Запустить: go run main.go → curl localhost:8080. Это работает, но nil означает «использовать DefaultServeMux» — глобальный мультиплексор, антипаттерн в продакшене.
Production-style сервер с явным мультиплексором¶
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser)
mux.HandleFunc("POST /users", handleCreateUser)
mux.HandleFunc("GET /health", handleHealth)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
Маршрутизация на Go 1.22+¶
В Go 1.22 (февраль 2024) стандартный http.ServeMux научился двум важным вещам:
- Сопоставление по HTTP-методу:
mux.HandleFunc("GET /users/{id}", handleGetUser)
mux.HandleFunc("DELETE /users/{id}", handleDeleteUser)
- Параметры пути:
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintln(w, "user id:", id)
})
До этого приходилось ставить сторонние роутеры (gorilla/mux, chi, gin). Сейчас для большинства задач хватает стандартного ServeMux. Но для middleware-цепочек удобнее всё-таки chi.
Обработчик: интерфейс или функция¶
// Стиль 1: функция
func handler(w http.ResponseWriter, r *http.Request) { ... }
mux.HandleFunc("/path", handler)
// Стиль 2: объект, реализующий http.Handler
type Server struct {
db *sql.DB
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ... }
mux.Handle("/path", server)
Когда обработчику нужны зависимости (БД, конфиг, логгер) — удобно сделать его методом структуры:
type API struct {
db *sql.DB
logger *slog.Logger
}
func (a *API) handleUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var u User
err := a.db.QueryRowContext(r.Context(), "SELECT ... WHERE id=$1", id).
Scan(&u.ID, &u.Name)
if err != nil {
http.Error(w, "not found", 404)
return
}
json.NewEncoder(w).Encode(u)
}
api := &API{db: db, logger: logger}
mux.HandleFunc("GET /users/{id}", api.handleUser)
Это идиоматичный способ DI в Go — без рефлексии и фреймворков.
Чтение тела запроса¶
func handleCreate(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
// ограничиваем размер тела на случай злонамеренного клиента
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad json", 400)
return
}
// ... обработка req ...
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
http.MaxBytesReader — обязательно для публичных API. Без него один клиент может прислать многогигабайтное тело и съесть память.
Чтение query-параметров¶
q := r.URL.Query()
limit := q.Get("limit") // строка
search := q.Get("search")
tags := q["tag"] // []string — для повторяющихся параметров (?tag=a&tag=b)
Middleware¶
Middleware — функция, которая принимает http.Handler и возвращает новый http.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 !validToken(token) {
http.Error(w, "unauthorized", 401)
return
}
ctx := context.WithValue(r.Context(), userIDKey{}, userIDFromToken(token))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
handler := loggingMiddleware(authMiddleware(mux))
srv := &http.Server{Addr: ":8080", Handler: handler}
В стандартной библиотеке нет helper'а для цепочек, но они тривиально пишутся вручную. Или возьмите chi — там удобный r.Use(...).
Graceful shutdown¶
В продакшене сервер должен:
- Принять
SIGINT/SIGTERM. - Перестать принимать новые запросы.
- Дать активным запросам завершиться (с разумным таймаутом).
- Закрыть БД, отправить буферы логов, выйти.
func main() {
mux := http.NewServeMux()
// ... регистрация handler'ов ...
srv := &http.Server{Addr: ":8080", Handler: mux}
// Запускаем сервер в горутине
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}()
log.Println("listening on :8080")
// Ждём сигнал
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("forced shutdown: %v", err)
}
log.Println("server stopped")
}
srv.Shutdown(ctx) — корректная остановка: закроется listener, активные соединения дождутся, либо принудительно оборвутся по истечении ctx.
TLS¶
Простейший HTTPS:
srv := &http.Server{
Addr: ":443",
Handler: mux,
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
В реальной жизни TLS-терминацию обычно делают на уровне reverse proxy (nginx, Caddy, Traefik) или балансировщика, а Go-приложение слушает HTTP внутри сети. Но если нужно «прямо так» — стандартная библиотека всё умеет, включая автоматический Let's Encrypt через golang.org/x/crypto/acme/autocert.
Тестирование HTTP¶
Стандартный пакет net/http/httptest — для запуска тестового сервера без поднятия настоящего сокета:
import "net/http/httptest"
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
handler(w, req)
if w.Code != 200 {
t.Errorf("got %d, want 200", w.Code)
}
if !strings.Contains(w.Body.String(), "Привет") {
t.Errorf("unexpected body: %s", w.Body.String())
}
}
Или полноценный тестовый сервер с реальным портом:
Параллель с Python¶
| Python | Go |
|---|---|
requests.get(url, timeout=5) |
http.Client{Timeout: 5*time.Second}.Get(url) |
requests.Session() |
http.Client (переиспользуется автоматически) |
aiohttp (async) |
net/http (горутины — встроенная конкурентность) |
Flask @app.route("/users/<id>") |
mux.HandleFunc("GET /users/{id}", ...) |
| FastAPI с зависимостями | методы на структуре + явная DI |
| WSGI / ASGI | http.Handler интерфейс |
app.before_request, middleware Flask |
функция-обёртка над http.Handler |
gunicorn/uvicorn |
один бинарник — srv.ListenAndServe() |
graceful shutdown через signal |
srv.Shutdown(ctx) |
Итог¶
Стандартный net/http — production-ready: HTTP-клиент с таймаутами, сервер с роутингом и параметрами пути (Go 1.22+), middleware как функции-обёртки, graceful shutdown через srv.Shutdown(ctx). Для большинства проектов сторонние фреймворки не нужны. В следующей лекции — файлы, JSON и работа с базами данных через database/sql.