Упрощаем написание HTTP обработчиков на Golang
При обработке входящего HTTP запроса требуется выполнить большое количество действий, таких как:
- Логирование входящего HTTP запроса
- Проверка на допустимость HTTP метода
- Выполнение аутентификации (basic, MS AD, . )
- Проверка валидности token (при необходимости)
- Считывание тела (body) входящего запроса
- Считывание заголовка (header) входящего запроса
- Собственно обработка запроса и формирование ответа
- Установка HSTS Strict-Transport-Security
- Установка Content-Type для исходящего ответа (response)
- Логирование исходящего HTTP ответа
- Запись заголовка (header) исходящего ответа
- Запись тела исходящего ответа
- Обработка и логирование ошибок
- Обработка defer recovery для восстановления после возможной panic
Большинство этих действий являются типовыми и не зависят от типа запроса и его обработки.
Для продуктивной эксплуатации на каждое действие необходимо предусмотреть обработку ошибок и логирование.
Повторять все это в каждом HTTP обработчике крайне неэффективно.
Даже если вынести весь код в отдельные подфункции, все равно получается примерно по 80-100 строк кода на каждый HTTP обработчик без учета собственно обработки запроса и формирования ответа.
Ниже описан используемый мной подход, по упрощению написание HTTP обработчиков на Golang без использования кодогенераторов и сторонних библиотек.
Этот подход был скомпонован из различных источников и рекомендаций в интернете.
У любого разработчика, естественно, есть свои устоявшиеся приемы и наработки — буду рад, если кто-то поделится своими подходами.
Подход к организации HTTP обработчиков
На следующем рисунке показана упрощенная UML диаграмма выполнения HTTP обработчика на примере некоего EchoHandler.
Идея, заложенная в предлагаемый подход, достаточно простая:
- На верхнем уровне необходимо внедрить defer фукнцию для восстановления после паники. На UML диаграмме — это анонимная функция RecoverWrap.func1, показаная красным цветом.
- Весь типовой код необходимо вынести в отдельный обработчик. Этот обработчик встраивается в наш HTTP handler. На UML диаграмме это функция Process — показана синим цветом.
- Собственно код функциональной обработки запроса и формирования ответа вынесен в анонимную функцию в нашем HTTP handler. На UML диаграмме это функция EchoHandler.func1 — показана зеленым цветом.

Пример кода
Полный код приведен в репозитории.
Ниже пример кода для HTTP обработчика EchoHandler, который «зеркалит» вход на выход.
При регистрации обработчика в роутере, регистрируется не собственно обработчик EchoHandler, а анонимная функция обработки паники (она возвращается функцией RecoverWrap), которая уже вызывает наш обработчик EchoHandler.
router.HandleFunc("/echo", service.RecoverWrap(http.HandlerFunc(service.EchoHandler))).Methods("GET")
Текст функции RecoverWrap для регистрации анонимной функции обработки паники.
После объявления defer func() запускается собственно наш обработчик EchoHandler.
func (s *Service) RecoverWrap(handlerFunc http.HandlerFunc) http.HandlerFunc < return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) < // объявляем функцию восстановления после паники defer func() < var myerr error r := recover() if r != nil < msg := "HTTP Handler recover from panic" switch t := r.(type) < case string: myerr = myerror.New("8888", msg, t) case error: myerr = myerror.WithCause("8888", msg, t) default: myerr = myerror.New("8888", msg) >// расширенное логирование ошибки в контексте HTTP s.processError(myerr, w, http.StatusInternalServerError, 0) > >() // вызываем обработчик if handlerFunc != nil < handlerFunc(w, r) >>) >
Собственно, код обработчика EchoHandler. В общем случае, он запускает функцию типовой обработки HTTP запроса Process и передается ей в качестве параметра анонимную функцию обработки.
func (s *Service) EchoHandler(w http.ResponseWriter, r *http.Request) < // Запускаем универсальный обработчик HTTP запросов _ = s.Process("POST", w, r, func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error) < header := Header<>// заголовок ответа // Считаем параметры из заголовка входящего запроса и поместим их в заголовок ответа for key := range r.Header < header[key] = r.Header.Get(key) >// возвращаем буфер запроса в качестве ответа, заголовок ответа и статус return requestBuf, header, http.StatusOK, nil >) >
Функция типовой обработки HTTP запроса Process. Входные параметры функции:
- method string — HTTP метод обработчика используется для проверки входящего HTTP запроса
- w http.ResponseWriter, r *http.Request — стандартные переменные для обработчика
- fn func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error) — собственно функция обработки, на вход она принимает буфер входящего запроса и уникальный номер HTTP request (для целей логирования), возвращает подготовленный буфер исходящего ответа, header исходящего ответа, HTTP статус и ошибку.
func (s *Service) Process(method string, w http.ResponseWriter, r *http.Request, fn func(requestBuf []byte, reqID uint64) ([]byte, Header, int, error)) error < var myerr error // Получить уникальный номер HTTP запроса reqID := GetNextRequestID() // Логируем входящий HTTP запрос if s.logger != nil < _ = s.logger.LogHTTPInRequest(s.сtx, r, reqID) // При сбое HTTP логирования, делаем системное логирование, но работу не останавливаем mylog.PrintfDebugMsg("Logging HTTP in request: reqID", reqID) >// Проверим разрешенный метод mylog.PrintfDebugMsg("Check allowed HTTP method: reqID, request.Method, method", reqID, r.Method, method) if r.Method != method < myerr = myerror.New("8000", "HTTP method is not allowed: reqID, request.Method, method", reqID, r.Method, method) mylog.PrintfErrorInfo(myerr) return myerr >// Если включен режим аутентификации без использования JWT токена, то проверять пользователя и пароль каждый раз mylog.PrintfDebugMsg("Check authentication method: reqID, AuthType", reqID, s.cfg.AuthType) if (s.cfg.AuthType == "INTERNAL" || s.cfg.AuthType == "MSAD") && !s.cfg.UseJWT < mylog.PrintfDebugMsg("JWT is of. Need Authentication: reqID", reqID) // Считаем из заголовка HTTP Basic Authentication username, password, ok := r.BasicAuth() if !ok < myerr := myerror.New("8004", "Header 'Authorization' is not set") mylog.PrintfErrorInfo(myerr) return myerr >mylog.PrintfDebugMsg("Get Authorization header: username", username) // Выполняем аутентификацию if myerr = s.checkAuthentication(username, password); myerr != nil < mylog.PrintfErrorInfo(myerr) return myerr >> // Если используем JWT - проверим токен if s.cfg.UseJWT < mylog.PrintfDebugMsg("JWT is on. Check JSON web token: reqID", reqID) // Считаем token из requests cookies cookie, err := r.Cookie("token") if err != nil < myerr := myerror.WithCause("8005", "JWT token does not present in Cookie. You have to authorize first.", err) mylog.PrintfErrorInfo(myerr) return myerr >// Проверим JWT в token if myerr = myjwt.CheckJWTFromCookie(cookie, s.cfg.JwtKey); myerr != nil < mylog.PrintfErrorInfo(myerr) return myerr >> // Считаем тело запроса mylog.PrintfDebugMsg("Reading request body: reqID", reqID) requestBuf, err := ioutil.ReadAll(r.Body) if err != nil < myerr = myerror.WithCause("8001", "Failed to read HTTP body: reqID", err, reqID) mylog.PrintfErrorInfo(myerr) return myerr >mylog.PrintfDebugMsg("Read request body: reqID, len(body)", reqID, len(requestBuf)) // вызываем обработчик mylog.PrintfDebugMsg("Calling external function handler: reqID, function", reqID, fn) responseBuf, header, status, myerr := fn(requestBuf, reqID) if myerr != nil < mylog.PrintfErrorInfo(myerr) return myerr >// use HSTS Strict-Transport-Security if s.cfg.UseHSTS < w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") >// Логируем ответ в файл if s.logger != nil < mylog.PrintfDebugMsg("Logging HTTP out response: reqID", reqID) _ = s.logger.LogHTTPOutResponse(s.сtx, header, responseBuf, status, reqID) // При сбое HTTP логирования, делаем системное логирование, но работу не останавливаем >// Записываем заголовок ответа mylog.PrintfDebugMsg("Set HTTP response headers: reqID", reqID) if header != nil < for key, h := range header < w.Header().Set(key, h) >> // Записываем HTTP статус ответа mylog.PrintfDebugMsg("Set HTTP response status: reqID, Status", reqID, http.StatusText(status)) w.WriteHeader(status) // Записываем тело ответа if responseBuf != nil && len(responseBuf) > 0 < mylog.PrintfDebugMsg("Writing HTTP response body: reqID, len(body)", reqID, len(responseBuf)) respWrittenLen, err := w.Write(responseBuf) if err != nil < myerr = myerror.WithCause("8002", "Failed to write HTTP repsonse: reqID", err) mylog.PrintfErrorInfo(myerr) return myerr >mylog.PrintfDebugMsg("Written HTTP response: reqID, len(body)", reqID, respWrittenLen) > return nil >
GauthamBanasandra / print-headers-401.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| package main |
| import ( |
| «net/http» |
| «fmt» |
| «time» |
| ) |
| func handler ( w http. ResponseWriter , r * http. Request ) |
| fmt . Printf ( «Request at %v \n » , time . Now ()) |
| for k , v := range r . Header |
| fmt . Printf ( «%v: %v \n » , k , v ) |
| > |
| if r . Header . Get ( «Authorization» ) == «» |
| w . WriteHeader ( http . StatusUnauthorized ) |
| w . Header (). Set ( «WWW-Authenticate» , `Digest realm=»testrealm@host.com», qop=»auth,auth-int», nonce=»dcd98b7102dd2f0e8b11d0f600bfb0c093″, opaque=»5ccc069c403ebaf9f0171e9517f40e41″` ) |
| > |
| > |
| func main () |
| http . HandleFunc ( «/» , handler ) |
| http . ListenAndServe ( «:9090» , nil ) |
| > |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| package main |
| import ( |
| «net/http» |
| «fmt» |
| «time» |
| ) |
| func handler ( w http. ResponseWriter , r * http. Request ) |
| fmt . Printf ( «Request at %v \n » , time . Now ()) |
| for k , v := range r . Header |
| fmt . Printf ( «%v: %v \n » , k , v ) |
| > |
| > |
| func main () |
| http . HandleFunc ( «/» , handler ) |
| http . ListenAndServe ( «:9090» , nil ) |
| > |
Тестирование HTTP хендлеров в Go
Вы пишите веб-сервис на Go и, конечно же, вы хотите тестировать ваши хендлеры. У вас используется пакет net/http, но вы не знаете как протестировать возврат корректных кодов в ваших хендлерах, правильность заголовков HTTP и насколько верно был сформирован ответ клиенту.
Давайте обсудим эти вопросы и посмотрим как можно писать более тестируемые приложения с использованием внедрения зависимостей и моков.
Стандартные хендлеры
Для начала, напишем простой тест, который будет проверят код ответа. В нашем случает это должен быть 200 ответ.
// handlers.go package handlers // http.HandleFunc("/health-check", HealthCheckHandler) func HealthCheckHandler(w http.ResponseWriter, r *http.Request) < // Очень простой хендлер проверки состояния. w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") // В будущем мы хотим сообщать сообщать о состоянии // базы данных или кеша (например Redis) выполняя // простой PING и отдавать все это в запросе io.WriteString(w, ``) >
// handlers_test.go package handlers import ( "net/http" "testing" ) func TestHealthCheckHandler(t *testing.T) < // Создаем запрос с указанием нашего хендлера. Нам не нужно // указывать параметры, поэтому вторым аргументом передаем nil req, err := http.NewRequest("GET", "/health-check", nil) if err != nil < t.Fatal(err) >// Мы создаем ResponseRecorder(реализует интерфейс http.ResponseWriter) // и используем его для получения ответа rr := httptest.NewRecorder() handler := http.HandlerFunc(HealthCheckHandler) // Наш хендлер соответствует интерфейсу http.Handler, а значит // мы можем использовать ServeHTTP и напрямую указать // Request и ResponseRecorder handler.ServeHTTP(rr, req) // Проверяем код if status := rr.Code; status != http.StatusOK < t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) >// Проверяем тело ответа expected := `` if rr.Body.String() != expected < t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) >>
Как видите, Go’шные пакеты testing и httptest делают тестирование наших хендлеров очень простым. Мы подготавливаем *http.Request , a *httptest.ResponseRecorder и затем используем их для проверки работы самого хендлера(коды, тело ответа и т.д.).
Если ваш хендлер ожидает определенные параметры, то это не проблема:
// например GET /api/projects?page=1&per_page=100 req, err := http.NewRequest("GET", "/api/projects", // url.Values это просто map[string][]string url.Values, "per_page": >) if err != nil < t.Fatal(err) >// Наш хенлер может ожидать определенный ключ для API req.Header.Set("Authorization", "Bearer abc123") // Заем вызываем handler.ServeHTTP(rr, req) как в примере выше.
Кроме того, если есть необходимость проверить как ваш хендлер или миделваре изменяют и мутируют данные запроса, то можно создать анонимную функцию, у которой будет доступ к внешним переменным которые вы сможете проверить в рамках своего теста.
// Declare it outside the anonymous function var token string handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)< // ':=' operator so we don't shadow our token variable above. // Обратите внимание, что тут нужно обязательно использовать // '=' , а не ':=' чтобы избежать затенения переменной token = GetToken(r) // Мы устанавливаем заголовок как и в примере выше w.Header().Set("Content-Type", "application/json") >) // Запускаем хендлер, проверяем коды, тело ответа и т.д. if token != expectedToken < t.Errorf("token does not match: got %v want %v", token, expectedToken) >if ctype := rr.Header().Get("Content-Type"); ctype != "application/json") < t.Errorf("content type header does not match: got %v want %v", ctype, "application/json") >
Маленький лайфхак: для строк application/json или Content-Type можно использовать константы, тогда вам не прийдется заново набирать(и ошибаться), а пользоваться автоподстановкой. Опечатка в тесте может быть очень фатальна, потому что вы будете уверенны, что все протестировано и работает правильно.
Не забывайте, что нужно тестировать и отказы, и исключительные ситуации в том числе. Проверяйте, возвращает ли ваш хендлер ошибку, когда это необходимо (например HTTP 403, или HTTP 500)
Заполняем context.Context в тестах
Как быть с хендлерами, которые ожидают данные из context.Context? Есть ли способ руками создать контекст и заполнить его различными данными, например типом пользователя и токеном?
Предпложим, что у вас есть кастомный хендлер который предоставляет метод ServeHTTPC(context.Context, http.ResponseWriter, *http.Request) . В Go 1.7 context.Context добавят в http.Request и это сделает жизнь значительно проще.
Для примера ниже я буду использовать Goji роутер. Он предоставляет возможность использовать хендлеры с context.Context . Тем не менее, описанный способ подойдет для большинства роутеров/фрейиворков, которые работают с context.Context .
func TestGetProjectsHandler(t *testing.T) < req, err := http.NewRequest("GET", "/api/users", nil) if err != nil < t.Fatal(err) >rr := httptest.NewRecorder() // например func GetUsersHandler(ctx context.Context, // w http.ResponseWriter, r *http.Request) goji.HandlerFunc(GetUsersHandler) // Создаем новый context.Context и заполняем его данными ctx = context.Background() ctx = context.WithValue(ctx, "app.auth.token", "abc123") ctx = context.WithValue(ctx, "app.user", &YourUser) // Указываем на контекст *http.Request и ResponseRecorder. handler.ServeHTTPC(ctx, rr, req) // Проверяем код, тело ответа и т.д. if status := rr.Code; status != http.StatusOK < t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) >// Тут мы можем проверить какие данные изменились/добавились // в нашем context.Contex if id , ok := ctx.Value("app.req.id").(string); !ok < t.Errorf("handler did not populate the request ID: got %v", id) >>
Мокаем обращения к базе данных
Наши хендлеры используют некоторый интерфейс datastore.ProjectStore с тремя методами ( Create , Get , Delete ). Мы можем замокать этот интерфейс для наших тестов определенным образом и проверить правильно ли отдаются HTTP коды.
Если вы хотите больше узнать о использовании интерфейсов для абстракции работы с базой данных, то рекомендую прочитать статью Thoughtbot и статью от fAlex Edwards.
// handlers_test.go package handlers // Возвращает ошибки в каждом методе type badProjectStore struct < // Это конкретный тип, который реализует datastore.ProjectStore. // Мы встроили его сюда, у нас автоматически добавились необходимые // методы и теперь наш тип badProjectStore удовлетворяет // интерфейсу datastore.ProjectStore, без необходимости добавлять заглушки // каждый метод (в зависимости от теста у нас будет использоваться // только те или иные методы) *datastore.Project >func (ps *badProjectStore) CreateProject(project *datastore.Project) error < return datastore.NetworkError> func (ps *badProjectStore) GetProject(id string) (*datastore.Project, error) < return nil, datastore.NetworkError > func TestGetProjectsHandlerError(t *testing.T) < var store datastore.ProjectStore = &badProjectStore<>// Мы выполняем внедрение нашего окружения в хендлеры. // Ref: http://elithrar.github.io/article/http-handler-error-handling-revisited/ env := handlers.Env req, err := http.NewRequest("GET", "/api/projects", nil) if err != nil < t.Fatal(err) >rr := httptest.Recorder() // Handler это кастомный тип, который работает с env and a http.Handler // GetProjectsHandler обращается к GetProject и должен вернуть 500 // в случае ошибки. Handler // Мы должны проверить, что в теле JSON тоже есть упоминание ошибки. expected := []byte(``) if !bytes.Equals(rr.Body.Bytes(), expected)
Пример выше несколько переусложнен, но нужно обратить внимание на несколько ключевых моментов:
- В нашей заглушке нет никакой работы с базой данных. Модульные тесты в пакете handlers не должны ничего знать про базу.
- Мы создали заглушку, которая возвращает ошибку и это позволило проверить работу нашего хендлера в исключительной ситуации. Мы проверили какой код возвращается и что пишется в тело ответа.
- Как вы могли догадаться, можно добавить «хорошую» заглушку на базе *datastore.Project и протестировать в таком виде, например выполнить кодирование/декодирование в JSON. Таким образом, мы могли бы отловить ситуации, когда внесение изменений может сломать совместимость с encoding/json .
Что дальше?
Конечно, это не исчерпывающий туториал, но после прочтения этой статьи вы будете знать с чего начинать. Если вы застряли на более сложном и комплексном примере, обратитесь к сообществу или посмотрите исходники пакетов, которые используют пакет httptest via GoDoc.
Настройка HTTP заголовков веб-приложения
Давайте обновим наше приложение, чтобы маршрут /snippet/create отвечал только на HTTP запросы, в которых используется метод POST .
Архитектура HTTP-маршрутов веб-приложения для создания новой заметки.

Премиум канал по Golang
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании!
Уроки, статьи и Видео
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
| Метод | Шаблон | Обработчик | Действие |
| ANY (любой метод) | / | home | Отображает домашнюю страницу |
| ANY (любой метод) | /snippet | showSnippet | Отображает определенную заметку |
| POST | /snippet/create | createSnippet | Создает новую заметку |
Очень важно выполнить данное изменение, потому что POST запрос к маршруту /snippet/create должен создавать новую запись в базе данных, но если будет выполнен GET запрос, то результатом будет ошибка 405 — запрещенный метод.
Создание новой заметки в базе данных является не идемпотентным действием, которое изменяет состояние нашего сервера. Поэтому мы должны придерживаться хорошей HTTP практики и ограничить маршрут, чтобы он отвечал только на POST-запросы.
Однако, главная причина, почему я хочу осветить данную тему заключается в HTTP заголовках и то, как они настраиваются.
Коды состояния HTTP в веб-приложениях
Давайте начнем с обновлением кода для функции-обработчика createSnippet() . Функция должна возвращать код состояния HTTP 405 (метод запрещен), но только в том случае, если это GET запрос. Для этого можно использовать метод w.WriteHeader() следующим образом:
package main
// Обработчик для создания новой заметки.
func createSnippet ( w http . ResponseWriter , r * http . Request ) <
// Используем r.Method для проверки, использует ли запрос метод POST или нет. Обратите внимание,
// что http.MethodPost является строкой и содержит текст "POST".
if r . Method != http . MethodPost <
// Если это не так, то вызывается метод w.WriteHeader() для возвращения статус-кода 405
// и вызывается метод w.Write() для возвращения тела-ответа с текстом "Метод запрещен".
// Затем мы завершаем работу функции вызвав "return", чтобы
// последующий код не выполнялся.
w . WriteHeader ( 405 )
w . Write ( [ ] byte ( "GET-Метод запрещен!" ) )
w . Write ( [ ] byte ( "Создание новой заметки. " ) )
Хотя данное изменение выглядит простым, в нем есть несколько нюансов, которые стоит пояснить:
- Вызвать метод w.WriteHeader() в обработчике можно только один раз, и после возвращения кода состояния HTTP, изменить его нельзя. Если попытаться вызвать w.WriteHeader() во второй раз, Go выдаст сообщение об ошибке;
- Если не вызывать метод w.WriteHeader() напрямую, тогда первый вызов w.Write() автоматически отправит пользователю код состояния 200 OK . Поэтому, если вы хотите вернуть другой код состояния, вызовите один раз метод w.WriteHeader() перед любым вызовом w.Write() .
Давайте рассмотрим все это в действии.
Перезапустите веб-сервер, откройте второе окно терминала и выполните следующую curl-команду для создания POST-запроса на адрес http://127.0.0.1:4000/snippet/create . Вы должны получить HTTP-ответ с кодом состояния 200 OK .
curl — i — X POST http : / / 127.0.0.1 : 4000 / snippet / create
Вы получите такой результат:
HTTP / 1.1 200 OK
Date : Tue , 15 Dec 2020 17 : 34 : 40 GMT
Content — Length : 45
Content — Type : text / plain ; charset = utf — 8
Создание новой заметки . . .
Однако, если вы выполните другой метод запроса: GET , PUT или DELETE — вы получите ответ с кодом состояния HTTP 405 Method Not Allowed .
curl — i — X PUT http : / / 127.0.0.1 : 4000 / snippet / create
HTTP / 1.1 405 Method Not Allowed
Date : Tue , 15 Dec 2020 17 : 36 : 52 GMT
Content — Length : 32
Content — Type : text / plain ; charset = utf — 8
GET — Метод запрещен !
Настройка HTTP-Заголовка
Другое улучшение, которое мы можем добавить, это заголовок Allow: POST для каждого ответа 405 «метод запрещен», чтобы пользователь знал, какие HTTP-методы поддерживаются для определенного URL.
Это можно сделать, используя метод w.Header().Set() для добавления нового заголовка к карте HTTP-заголовков. Например:
package main
// Обработчик для создания новой заметки.
func createSnippet ( w http . ResponseWriter , r * http . Request ) <
// Используем r.Method для проверки, использует ли запрос метод POST или нет. Обратите внимание,
// что http.MethodPost является строкой и содержит текст "POST".
if r . Method != http . MethodPost <
// Используем метод Header().Set() для добавления заголовка 'Allow: POST' в
// карту HTTP-заголовков. Первый параметр - название заголовка, а
// второй параметр - значение заголовка.
w . Header ( ) . Set ( "Allow" , http . MethodPost )
// Вызываем метод w.WriteHeader() для возвращения статус-кода 405
// и вызывается метод w.Write() для возвращения тела-ответа с текстом "Метод запрещен".
w . WriteHeader ( 405 )
w . Write ( [ ] byte ( "GET-Метод запрещен!" ) )
// Затем мы завершаем работу функции вызвав "return", чтобы
// последующий код не выполнялся.
w . Write ( [ ] byte ( "Создание новой заметки. " ) )
Внимание: Вы должны сперва вызвать метод w.Header().Set() и уже потом остальные методы w.WriteHeader() или w.Write() иначе ваши изменения не будут учтены.
Давайте повторим наш PUT или GET запрос на адрес /snippet/create и проверим если наш новый заголовок Allow появится в списке:
curl — i — X PUT http : / / 127.0.0.1 : 4000 / snippet / create
HTTP / 1.1 405 Method Not Allowed
Allow : POST
Date : Tue , 15 Dec 2020 18 : 14 : 10 GMT
Content — Length : 32
Content — Type : text / plain ; charset = utf — 8
GET — Метод запрещен !
Как вы видите, теперь ответ включает в себя строку Allow: POST .
Использование http.Error
Если требуется отправить какой-то код состояния, кроме 200 с текстом ответа (как мы это делали в коде выше), можно использовать http.Error(). Это вспомогательная функция, которая принимает текст сообщения и код состояния, а затем «за кулисами» сама вызывает методы w.WriteHeader() и w.Write() .
Обновим код следующим образом:
package main
// Обработчик для создания новой заметки.
func createSnippet ( w http . ResponseWriter , r * http . Request ) <
if r . Method != http . MethodPost <
w . Header ( ) . Set ( "Allow" , http . MethodPost )
// Используем функцию http.Error() для отправки кода состояния 405 с соответствующим сообщением.
http . Error ( w , "Метод запрещен!" , 405 )
w . Write ( [ ] byte ( "Создание новой заметки. " ) )
С точки зрения функциональности, она осталась прежней. Разница в том, что теперь мы передаем ответственность вызова http.ResponseWriter другой функции, которая отправляет ответ пользователю за нас.
В Go, практика вызова http.ResponseWriter в коде других вспомогательных функций очень распространена, и в будущих уроках мы будем часто использовать такой подход. Важно, чтобы вы понимали, что лежит «под капотом» в других более продвинутых (и интересных!) способах отправки ответов пользователям. Ранее, мы использовали методы w.Write() и w.WriteHeader() напрямую, но на практике так делают довольно редко.
Управление HTTP заголовками в Go
В приведенном выше коде мы использовали w.Header().Set() , чтобы добавить новый заголовок в карту HTTP заголовка. Существуют такие методы как Add() , Del() и Get() , которые тоже можно использовать для добавления, удаления и чтения заголовков из карты.
// Устанавливаем новый заголовок управления кешем. Если заголовок «Cache-Control» уже указан
// то он будет переписан.
w . Header ( ) . Set ( «Cache-Control» , «public, max-age=31536000» )
// Метод Add() добавляет новый заголовок «Cache-Control» и может
// вызываться несколько раз.
w . Header ( ) . Add ( «Cache-Control» , «public» )
w . Header ( ) . Add ( «Cache-Control» , «max-age=31536000» )
// Удаляем все значения из заголовка «Cache-Control».
w . Header ( ) . Del ( «Cache-Control» )
// Получаем первое значение из заголовка «Cache-Control».
w . Header ( ) . Get ( «Cache-Control» )
HTTP заголовки по умолчанию от веб-сервера
При отправке ответа, Go автоматически установит для вас три сгенерированных системой HTTP заголовка: Date , Content-Length и Content-Type .
Заголовок Content-Type (тип контента) весьма интересен. Go попытается автоматически узнать тип контента проанализировав содержимое тела ответа с помощью функции http.DetectContentType() . Если эта функция не сможет определить тип контента, Go укажет следующее значение для заголовка Content-Type: application/octet-stream .
Функция http.DetectContentType() обычно работает довольно неплохо. Однако главная проблема веб-разработчиков, плохо знакомыми с Go, заключается в том, что данная функция не может отличить JSON от обычного текста. По умолчанию, ответы содержащие JSON будут отправляться с заголовком Content-Type: text/plain; charset=utf-8 . Вы можете предотвратить это, установив правильный заголовок вручную следующим образом: