Irbisa · Шпаргалка

HTTP-сервисы и REST API

net/http, паттерны ServeMux, middleware, таймауты, graceful shutdown

Middle-разработчик · 23 вопроса

  1. 01

    Как написать минимальный HTTP-сервер на net/http и за что отвечают http.Handler, http.HandlerFunc и http.ServeMux?

    Под http.Handler подходит любой тип с методом ServeHTTP(w, r); http.HandlerFunc превращает обычную функцию в такой тип; http.ServeMux служит хендлером, который по паттерну направляет каждый запрос в другой хендлер.

    type itemsHandler struct {	store *Store} // itemsHandler is an http.Handler because it has ServeHTTP.func (h itemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {…
  2. 02

    Как в хендлере прочитать параметр пути, query-параметр, заголовок и JSON-тело и вернуть JSON-ответ?

    Значения пути берутся из r.PathValue("id"), query-параметры из r.URL.Query().Get("q"), заголовки из r.Header.Get(...), а тело приходит потоком, который декодируют через json.NewDecoder(r.Body).

    type renameReq struct {	Name string `json:"name"`} // Registered as mux.HandleFunc("PUT /items/{id}", renameItem).func renameItem(w http.ResponseWriter, r *http.Request) {…
  3. 03

    Этот хендлер должен ответить 201 Created с JSON-телом. Какой статус и Content-Type на самом деле получит клиент и почему?

    func createItem(w http.ResponseWriter, r *http.Request) {	item := Item{ID: 42, Name: "lamp"} 	json.NewEncoder(w).Encode(item)	w.WriteHeader(http.StatusCreated)	w.Header().Set("Content-Type", "application/json")}

    Клиент получит 200 OK с Content-Type: text/plain; charset=utf-8.

  4. 04

    Какие HTTP-методы безопасные и идемпотентные, чем PUT отличается от PATCH и какие коды ответа возвращать в типичных случаях?

    Безопасные методы (GET, HEAD, OPTIONS) не меняют состояние сервера; идемпотентные дают одинаковый эффект, отправлены они один раз или десять: это безопасные плюс PUT и DELETE.

  5. 05

    Что Go 1.22 добавил в маршрутизацию http.ServeMux и как mux выбирает хендлер, если подходят два паттерна?

    Теперь паттерн может содержать метод и wildcard-сегменты, например GET /items/{id}, а хендлер читает значение через r.PathValue("id").

    func routes(s *Server) *http.ServeMux {	mux := http.NewServeMux()	mux.HandleFunc("GET /items", s.listItems)	mux.HandleFunc("POST /items", s.createItem)	mux.HandleFunc("GET /items/new", s.newItemForm) // beats {id}: more specific	mux.HandleFunc("GET /items/{id}", s.getItem)…
  6. 06

    Как net/http обслуживает много запросов одновременно и что это значит для состояния, общего для хендлеров?

    Сервер обслуживает каждое соединение в отдельной горутине, а в HTTP/2 отдельную горутину получает ещё и каждый поток запроса, так что один хендлер выполняется параллельно для множества запросов.

  7. 07

    Что такое middleware в Go и как написать middleware, которое логирует метод, путь, статус и длительность и передаёт ID запроса по цепочке?

    Middleware представляет собой функцию func(http.Handler) http.Handler, которая оборачивает следующий хендлер и выполняет код до и после него.

    type ctxKey struct{} func RequestID(next http.Handler) http.Handler {	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {		id := r.Header.Get("X-Request-Id") // set by our own proxy		if id == "" {…
  8. 08

    В каком порядке выполняется цепочка middleware вида a(b(c(mux))) и где в ней должны стоять request ID, логирование, recovery, CORS и auth?

    Самая внешняя обёртка выполняется первой на входе и последней на выходе: в a(b(c(mux))) слой a видит запрос раньше b и c.

    // Chain(a, b, c)(h) == a(b(c(h))): a runs first on the way in.func Chain(mws ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {	return func(h http.Handler) http.Handler {		for i := len(mws) - 1; i >= 0; i-- {			h = mws[i](h)		}…
  9. 09

    Найдите баг: после паники в хендлере экспорта часть клиентов получает 200 с оборванным JSON, оканчивающимся на internal error.

    func recoverPanics(next http.Handler) http.Handler {	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {		defer func() {			if v := recover(); v != nil {				slog.Error("panic", "err", v)				http.Error(w, "internal error", http.StatusInternalServerError)			}		}()		next.ServeHTTP(w, r)	})}func export(w http.ResponseWriter, r *http.Request) {	fmt.Fprint(w, `{"rows":[1,2,`)	var m map[string]int	m["x"] = 1 // panics mid-response}

    Middleware отправляет http.Error, хотя хендлер уже записал часть тела, поэтому 500 не заменит зафиксированный 200: сервер пишет superfluous response.WriteHeader call, а internal error дописывается к недописанному JSON.

  10. 10

    Как безопасно декодировать JSON-тело запроса и что не так с body, _ := io.ReadAll(r.Body) и последующим json.Unmarshal?

    io.ReadAll буферизует всё, что прислал клиент, так что одно огромное тело может исчерпать память, а проигнорированная ошибка скрывает оборванную загрузку.

    func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB	dec := json.NewDecoder(r.Body)	dec.DisallowUnknownFields() 	if err := dec.Decode(dst); err != nil {…
  11. 11

    Как JSON API должен сообщать об ошибках и ошибках валидации по полям и почему http.Error(w, err.Error(), 500) для этого не подходит?

    Возвращайте стабильное JSON-тело ошибки в стандартной форме RFC 9457 problem details (application/problem+json) со статусом, который говорит, чья это вина.

    // RFC 9457 problem details.type Problem struct {	Title  string            `json:"title"`	Status int               `json:"status"`	Detail string            `json:"detail,omitempty"`	Errors map[string]string `json:"errors,omitempty"` // extension: field -> reason…
  12. 12

    Как бы вы разделили REST-сервис на Go на слои хендлеров, сервисов и репозиториев и как хендлеры получают свои зависимости?

    Хендлеры занимаются только HTTP, сервисный слой держит бизнес-правила, а репозиторий прячет базу данных.

    // Declared where it is used; *postgres.UserRepo satisfies it implicitly.type UserRepo interface {	ByID(ctx context.Context, id int64) (User, error)	Create(ctx context.Context, u User) (int64, error)}…
  13. 13

    Как бы вы спроектировали REST API для ресурса вроде постов блога: URL, методы, пагинацию и версионирование?

    Моделируйте существительные как ресурсы (/posts, /posts/{id}, /posts/{id}/comments), пусть действие передаёт HTTP-метод, возвращайте честные коды ответа, пагинируйте каждый список и версионируйте API так, чтобы существующие клиенты никогда не ломались.

  14. 14

    Что бы вы взяли для нового сервиса (стандартный mux из net/http, chi, gin, echo или fiber) и чем платите за каждый выбор?

    Начиная с Go 1.22 стандартный mux умеет методы и параметры пути, так что чистого net/http или chi, если нужны группы маршрутов и middleware на маршрут, хватает большинству сервисов.

  15. 15

    Как реализовать rate limiting в виде HTTP-middleware и что должен получить клиент, когда упёрся в лимит?

    Держите token bucket на каждого клиента (его даёт golang.org/x/time/rate) и проверяйте его в middleware до хендлера.

    type RateLimiter struct {	mu       sync.Mutex	limiters map[string]*rate.Limiter} func NewRateLimiter() *RateLimiter {…
  16. 16

    Какие таймауты должен задавать продакшен-http.Server и почему http.ListenAndServe(":8080", mux) опасен на публичном порту?

    http.ListenAndServe создаёт сервер со всеми таймаутами, равными нулю, поэтому клиент, который шлёт заголовки по байту в минуту (Slowloris), держит соединение и горутину вечно.

    func newServer(h http.Handler, logger *slog.Logger) *http.Server {	return &http.Server{		Addr:              ":8080",		Handler:           http.TimeoutHandler(h, 10*time.Second, "request timed out"),		ReadHeaderTimeout: 5 * time.Second,  // the Slowloris fix		ReadTimeout:       30 * time.Second, // headers + body…
  17. 17

    Как корректно остановить HTTP-сервер на Go по SIGTERM, не оборвав запросы, которые сейчас выполняются?

    Поймайте сигнал через signal.NotifyContext, затем вызовите srv.Shutdown(ctx) с новым контекстом с таймаутом: он перестаёт принимать соединения, закрывает простаивающие и ждёт завершения активных запросов.

    func run(handler http.Handler, db *sql.DB) error {	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)	defer stop() 	srv := &http.Server{Addr: ":8080", Handler: handler, ReadHeaderTimeout: 5 * time.Second}…
  18. 18

    Этот хендлер запускает медленный экспорт в фоне и отвечает 202 Accepted. Что с ним не так?

    func (s *Server) startExport(w http.ResponseWriter, r *http.Request) {	go func() {		data, err := s.buildExport(r.Context())		if err != nil {			log.Println(err)			return		}		w.Write(data)	}()	w.WriteHeader(http.StatusAccepted)}

    Горутина переживает запрос: r.Context() отменяется, как только хендлер возвращается, поэтому экспорт обычно обрывается с context canceled, а писать в w после этого момента нельзя.

  19. 19

    Как сервису создавать свой http.Client и как задать каждому исходящему запросу собственный дедлайн?

    Создайте один *http.Client при старте с настроенным Transport и переиспользуйте его везде, потому что пул соединений живёт в Transport.

    // Built once, at startup, and shared.func newUpstreamClient() *http.Client {	tr := http.DefaultTransport.(*http.Transport).Clone()	tr.MaxIdleConnsPerHost = 100	return &http.Client{Transport: tr, Timeout: 5 * time.Second} // backstop}…
  20. 20

    Как стримить Server-Sent Events из хендлера на Go и почему события могут приходить все разом или стрим обрывается по WriteTimeout?

    Пишите события в формате text/event-stream и сбрасывайте каждое через http.NewResponseController(w).Flush().

    func (s *Server) events(w http.ResponseWriter, r *http.Request) {	rc := http.NewResponseController(w)	w.Header().Set("Content-Type", "text/event-stream")	w.Header().Set("Cache-Control", "no-cache")	w.Header().Set("X-Accel-Buffering", "no") // nginx: do not buffer this response	w.WriteHeader(http.StatusOK)…
  21. 21

    Клиент переиспользует один http.Client, но когда апстрим часто отвечает 404, почти каждый вызов открывает новое TCP- и TLS-соединение. Почему?

    func fetchUser(ctx context.Context, id string) (*User, error) {	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/"+id, nil)	resp, err := client.Do(req)	if err != nil {		return nil, err	}	defer resp.Body.Close()	if resp.StatusCode != http.StatusOK {		return nil, fmt.Errorf("upstream: %s", resp.Status)	}	var u User	return &u, json.NewDecoder(resp.Body).Decode(&u)}

    На ветке ошибки функция закрывает тело, не прочитав его.

  22. 22

    Как настроить http.Transport на тысячи запросов в секунду к одному апстриму и чем переиспользование соединений в HTTP/1.1 отличается от HTTP/2?

    Увеличьте MaxIdleConnsPerHost: по умолчанию он равен 2, поэтому при параллельной нагрузке почти каждое соединение после использования закрывается, а не возвращается в пул, и каждый запрос платит за новое TCP- и TLS-рукопожатие.

    // wrong: a bare Transport - HTTP/1.1 only, no proxy, an idle pool of 2 per host// client := &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}} func newHotClient(cfg *tls.Config) *http.Client {	tr := http.DefaultTransport.(*http.Transport).Clone()	tr.TLSClientConfig = cfg…
  23. 23

    Как сделать POST /charges безопасным для ретраев и какие запросы http.Transport в Go повторяет сам?

    Отправляйте один Idempotency-Key на операцию, общий для всех её повторов, а сервер пусть хранит результат под этим ключом, чтобы повтор возвращал сохранённый ответ, а не списывал деньги снова.

    func charge(ctx context.Context, c *http.Client, body []byte) (*http.Response, error) {	key := rand.Text() // one key for the operation, reused by every attempt	var lastErr error	for attempt := range 4 {		// A Request is single-use; bytes.NewReader sets GetBody so it can be replayed.		req, err := http.NewRequestWithContext(ctx, http.MethodPost, chargesURL, bytes.NewReader(body))…