Irbisa · Шпаргалка
HTTP-сервисы и REST API
net/http, паттерны ServeMux, middleware, таймауты, graceful shutdown
Как написать минимальный HTTP-сервер на
net/httpи за что отвечаютhttp.Handler,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) {…Как в хендлере прочитать параметр пути, query-параметр, заголовок и JSON-тело и вернуть JSON-ответ?
type renameReq struct { Name string `json:"name"`} // Registered as mux.HandleFunc("PUT /items/{id}", renameItem).func renameItem(w http.ResponseWriter, r *http.Request) {…Этот хендлер должен ответить
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")}Какие HTTP-методы безопасные и идемпотентные, чем
PUTотличается отPATCHи какие коды ответа возвращать в типичных случаях?Что Go 1.22 добавил в маршрутизацию
http.ServeMuxи как mux выбирает хендлер, если подходят два паттерна?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)…Как
net/httpобслуживает много запросов одновременно и что это значит для состояния, общего для хендлеров?Что такое middleware в Go и как написать middleware, которое логирует метод, путь, статус и длительность и передаёт ID запроса по цепочке?
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 == "" {…В каком порядке выполняется цепочка middleware вида
a(b(c(mux)))и где в ней должны стоять request ID, логирование, recovery, CORS и auth?// 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) }…Найдите баг: после паники в хендлере экспорта часть клиентов получает
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}Как безопасно декодировать JSON-тело запроса и что не так с
body, _ := io.ReadAll(r.Body)и последующимjson.Unmarshal?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 {…Как JSON API должен сообщать об ошибках и ошибках валидации по полям и почему
http.Error(w, err.Error(), 500)для этого не подходит?// 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…Как бы вы разделили REST-сервис на Go на слои хендлеров, сервисов и репозиториев и как хендлеры получают свои зависимости?
// 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)}…Как бы вы спроектировали REST API для ресурса вроде постов блога: URL, методы, пагинацию и версионирование?
Что бы вы взяли для нового сервиса (стандартный mux из
net/http, chi, gin, echo или fiber) и чем платите за каждый выбор?Как реализовать rate limiting в виде HTTP-middleware и что должен получить клиент, когда упёрся в лимит?
type RateLimiter struct { mu sync.Mutex limiters map[string]*rate.Limiter} func NewRateLimiter() *RateLimiter {…Какие таймауты должен задавать продакшен-
http.Serverи почемуhttp.ListenAndServe(":8080", mux)опасен на публичном порту?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…Как корректно остановить HTTP-сервер на Go по
SIGTERM, не оборвав запросы, которые сейчас выполняются?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}…Этот хендлер запускает медленный экспорт в фоне и отвечает
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)}Как сервису создавать свой
http.Clientи как задать каждому исходящему запросу собственный дедлайн?// 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}…Как стримить Server-Sent Events из хендлера на Go и почему события могут приходить все разом или стрим обрывается по
WriteTimeout?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)…Клиент переиспользует один
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)}Как настроить
http.Transportна тысячи запросов в секунду к одному апстриму и чем переиспользование соединений в HTTP/1.1 отличается от HTTP/2?// 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…Как сделать
POST /chargesбезопасным для ретраев и какие запросыhttp.Transportв Go повторяет сам?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))…