Одно из моих самых любимых нововведений в недавнем релизе Go 1.20 — это тип http.ResponseController, который может похвастаться тремя очень приятными полезностями:
-
Теперь вы можете переопределять ваши общесерверные таймауты/дедлайны чтения и записи новыми для каждого отдельного запроса.
-
Шаблон использования интерфейсов http.Flusher и http.Hijacker стал более понятным и менее сложным. Нам больше не нужны никакие утверждения типов!
-
Он делает проще и безопаснее создание и использование пользовательских реализаций 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/
Добавить комментарий