Для чего нужен тип http.ResponseController?

от автора

Одно из моих самых любимых нововведений в недавнем релизе Go 1.20 — это тип http.ResponseController, который может похвастаться тремя очень приятными полезностями:

  1. Теперь вы можете переопределять ваши общесерверные таймауты/дедлайны чтения и записи новыми для каждого отдельного запроса.

  2. Шаблон использования интерфейсов http.Flusher и http.Hijacker стал более понятным и менее сложным. Нам больше не нужны никакие утверждения типов!

  3. Он делает проще и безопаснее создание и использование пользовательских реализаций http.ResponseWriter.

Первые два преимущества упоминаются в описании изменений, которое прилагается к релизу, а третье, кажется, ускользнуло из всеобщего поля зрения… а жаль, потому что оно очень полезное!

Что ж, давайте взглянем на них поближе.

Таймауты для отдельных запросов

http.Server Go имеет настройки ReadTimeout и WriteTimeout, которые вы можете использовать для автоматического закрытия HTTP-соединения, если время, затраченное на чтение запроса или запись ответа, превышает какое-либо фиксированное значение. Эти настройки являются общесерверными и применяются ко всем запросам, независимо от обработчика или URL.

С появлением http.ResponseController вы теперь можете использовать методы SetReadDeadline() и SetWriteDeadline(), чтобы ослабить или, наоборот, ужесточить эти настройки для каждого конкретного запроса в зависимости от ваших потребностей. Например:

func exampleHandler(w http.ResponseWriter, r *http.Request) {     rc := http.NewResponseController(w)      // Установим таймаут записи в 5 секунд.     err := rc.SetWriteDeadline(time.Now().Add(5 * time.Second))     if err != nil {         // Обработка ошибки     }      // Делаем здесь что-нибудь...      // Записываем ответ как обычно     w.Write([]byte("Done!")) }

Это особенно полезно для приложений, содержащих небольшое количество обработчиков, которым требуются более длительные таймауты, чем всем остальным, для таких вещей, как обработка загрузки файла или выполнение длительной операции.

Несколько деталей, о которых стоит упомянуть:

  • Если вы установите очень короткий общесерверный таймаут, и этот таймаут будет достигнут до того, как вы вызовете SetWriteDeadline() или SetReadDeadline(), то они не возымеют никакого эффекта. Общесерверный таймаут в этом случае побеждает.

  • Если ваш базовый http.ResponseWriter не поддерживает установку таймаутов для отдельных запросов, то вызов SetWriteDeadline() или SetReadDeadline() вернет ошибку http.ErrNotSupported.

  • Теперь вы можете отменять общесерверный таймаут для отдельных запросов, передав обнуленную структур time.Time в SetWriteDeadlin() или SetReadDeadline(). Например:

rc := http.NewResponseController(w) err := rc.SetWriteDeadline(time.Time{}) if err != nil {     // Обработка ошибки }

Интерфейсы Flusher и Hijacker

Тип http.ResponseController также делает более удобным использование «опциональных» интерфейсов http.Flusher и http.Hijacker. Например, до Go 1.20, чтобы отправить данные ответа клиенту, вы могли использовать кода следующего вида:

func exampleHandler(w http.ResponseWriter, r *http.Request) {     f, ok := w.(http.Flusher)     if !ok {         // Обработка ошибки     }      for i := 0; i < 5; i++ {         fmt.Fprintf(w, "Write %d\n", i)         f.Flush()          time.Sleep(time.Second)     } }

Теперь вы можете сделать это так:

func exampleHandler(w http.ResponseWriter, r *http.Request) {     rc := http.NewResponseController(w)      for i := 0; i < 5; i++ {         fmt.Fprintf(w, "Write %d\n", i)         err := rc.Flush()         if err != nil {             // Обработка ошибки         }          time.Sleep(time.Second)     } }

Шаблонный код перехвата (hijacking) соединения аналогичен:

func (app *application) home(w http.ResponseWriter, r *http.Request) {     rc := http.NewResponseController(w)      conn, bufrw, err := rc.Hijack()     if err != nil {         // Обработка ошибки     }     defer conn.Close()      // Делаем здесь что-нибудь... }

Опять же, если ваш базовый http.ResponseWriter не поддерживает flush или перехват соединения, то вызов Flush() или Hijack() в http.ResponseController также вернет ошибку http.ErrNotSupported.

Пользовательские http.ResponseWriter’ы

Теперь также проще и безопаснее создавать и использовать пользовательские реализации http.ResponseWriter, которые поддерживают flush и перехват соединения.

Вероятно, проще всего объяснить, как это работает, на примере, поэтому давайте посмотрим на код пользовательской реализации http.ResponseWriter, которая записывает код состояния HTTP ответа.

type statusResponseWriter struct {     http.ResponseWriter // Встраиваем a http.ResponseWriter     statusCode    int     headerWritten bool }  func newstatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {     return &statusResponseWriter{         ResponseWriter: w,         statusCode:     http.StatusOK,     } }  func (mw *statusResponseWriter) WriteHeader(statusCode int) {     mw.ResponseWriter.WriteHeader(statusCode)      if !mw.headerWritten {         mw.statusCode = statusCode         mw.headerWritten = true     } }  func (mw *statusResponseWriter) Write(b []byte) (int, error) {     mw.headerWritten = true     return mw.ResponseWriter.Write(b) }  func (mw *statusResponseWriter) Unwrap() http.ResponseWriter {     return mw.ResponseWriter }

Итак, здесь мы определили пользовательский тип statusResponseWriter, который встраивает уже существующий тип http.ResponseWriter и реализует пользовательские методы WriteHeader() и Write() для записи кода состояния HTTP ответа.

Но на что здесь стоит обратить внимание, так это на метод Unwrap() в конце, который возвращает исходный встроенный http.ResponseWriter.

Когда вы используете новый тип http.ResponseController, чтобы сделать flush, перехватить соединение или установить таймаут, он вызовет этот метод Unwrap(), чтобы получить доступа к исходному http.ResponseWriter. При необходимости это делается рекурсивно, поэтому вы потенциально можете наслаивать несколько пользовательских реализации http.ResponseWriter друг на друга.

Давайте рассмотрим полный пример, где мы используем этот statusResponseWriter в сочетании с некоторым middleware для логирования кодов состояния ответа, а также двумя обработчиками, один из которых отправляет «нормальный» ответ, а другой – задействует новый тип http.ResponseController, чтобы сделать flush.

package main  import (     "log"     "net/http"     "time" )  type statusResponseWriter struct {     http.ResponseWriter // Встраиваем a http.ResponseWriter     statusCode    int     headerWritten bool }  func newstatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {     return &statusResponseWriter{         ResponseWriter: w,         statusCode:     http.StatusOK,     } }  func (mw *statusResponseWriter) WriteHeader(statusCode int) {     mw.ResponseWriter.WriteHeader(statusCode)      if !mw.headerWritten {         mw.statusCode = statusCode         mw.headerWritten = true     } }  func (mw *statusResponseWriter) Write(b []byte) (int, error) {     mw.headerWritten = true     return mw.ResponseWriter.Write(b) }  func (mw *statusResponseWriter) Unwrap() http.ResponseWriter {     return mw.ResponseWriter }  func main() {     mux := http.NewServeMux()     mux.HandleFunc("/normal", normalHandler)     mux.HandleFunc("/flushed", flushedHandler)      log.Print("Listening...")     err := http.ListenAndServe(":3000", logResponse(mux))     if err != nil {         log.Fatal(err)     } }   func logResponse(next http.Handler) http.Handler {     return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {         sw := newstatusResponseWriter(w)         next.ServeHTTP(sw, r)         log.Printf("%s %s: status %d\n", r.Method, r.URL.Path, sw.statusCode)     }) }  func normalHandler(w http.ResponseWriter, r *http.Request) {     w.WriteHeader(http.StatusTeapot)     w.Write([]byte("OK")) }  func flushedHandler(w http.ResponseWriter, r *http.Request) {     rc := http.NewResponseController(w)      w.Write([]byte("Write A...."))         err := rc.Flush()     if err != nil {         log.Println(err)         return     }      time.Sleep(time.Second)      w.Write([]byte("Write B...."))     err = rc.Flush()     if err != nil {         log.Println(err)     } }

Если хотите, вы можете запустить этот код и попробовать отправить запросы к конечным точкам /normal и /flushed:

$ curl http://localhost:3000/normal OK  $ curl --no-buffer http://localhost:3000/flushed Write A....Write B....

Вы должны увидеть ответ от flushedHandler в двух частях, где первая часть – Write A…, и через секунду вторая часть Write B….

И вы также должны увидеть, что statusResponseWriter и middleware logResponse успешно составили лог, где будут корректные коды состояния HTTP для каждого ответа.

$ go run main.go  2023/03/06 21:41:21 Listening... 2023/03/06 21:41:32 GET /normal: status 418 2023/03/06 21:41:44 GET /flushed: status 200

Если вам понравилась эта статья, вы можете ознакомиться с моим списком рекомендуемых туториаловов или почитать мои книги Let’s Go и Let’s Go Further, которые научат вас всему, что вам нужно знать о том, как создавать профессиональные готовые к работе в производственной среде веб-приложения и API-интерфейсы с помощью Go.

Материал подготовлен в преддверии старта онлайн-курса «Golang Developer. Professional».


ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/736980/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *