Приложение на Go шаг за шагом. Часть 4: отправка сообщений об ошибках
- среда, 11 февраля 2026 г. в 00:00:12
Привет! Я Владислав Попов, автор курса «Go-разработчик с нуля» в Яндекс Практикуме. В серии статей я хочу помочь начинающим разработчикам упорядочить знания и написать приложение на Go с нуля: мы вместе пройдём каждый шаг и создадим API для получения информации о книгах и управления ими.
На данном этапе наш API отправляет хорошо отформатированные JSON-ответы на успешные запросы, но если клиент отправляет некорректный запрос или в приложении что-то идёт не так, он получает текстовое сообщение об ошибке из функций http.Error() и http.NotFound(). В этой статье мы исправим это, научив API отправлять все ответы, включая ошибки, в формате JSON.
Итак, сейчас, если что-то идёт не так, приложение отправляет текстовое сообщение об ошибке из функций http.Error() и http.NotFound(). Давайте это исправим, создав несколько дополнительных помощников для обработки ошибок и отправки соответствующих ответов в формате JSON.
Для этого создайте новый файл cmd/api/errors.go:
$ touch cmd/api/errors.go
И добавьте несколько вспомогательных методов:
// cmd/api/errors.go package main import ( "fmt" "net/http" ) // logError() - универсальный вспомогательный инструмент для записи сообщений об ошибках. func (app *application) logError(r *http.Request, err error) { app.logger.Println(err) } // ErrorResponse() является универсальным вспомогательным средством для отправки сообщений // об ошибке в формате JSON с заданным кодом состояния. Обратите внимание, что мы используем interface{} // для параметра message, а не просто строковый тип. Это дает нам // больше гибкости в выборе значений, которые мы можем включить в ответ. func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { env := envelope{"error": message} err := app.writeJSON(w, status, env, nil) if err != nil { app.logError(r, err) w.WriteHeader(500) } } // serverErrorResponse() будет использоваться, когда наше приложение столкнётся с // непредвиденной проблемой во время выполнения. Он выводит подробное сообщение // об ошибке, а затем с помощью вспомогательного метода errorResponse() отправляет // клиенту код состояния 500 Internal Server Error и JSON-ответ (содержащий общее // сообщение об ошибке). func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { app.logError(r, err) message := "the server encountered a problem and could not process your request" app.errorResponse(w, r, http.StatusInternalServerError, message) } // notFoundResponse() используется для отправки клиенту кода состояния 404 Not Found и // ответа в формате JSON. func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { message := "the requested resource could not be found" app.errorResponse(w, r, http.StatusNotFound, message) } // methodNotAllowedResponse() используется для отправки клиенту кода состояния 405 Method Not Allowed // и ответа в формате JSON. func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { message := fmt.Sprintf("the %s method is not supported for this resource", r.Method) app.errorResponse(w, r, http.StatusMethodNotAllowed, message) }
Теперь, когда всё готово, давайте обновим наши обработчики API, чтобы они использовали эти новые вспомогательные функции вместо http.Error() и http.NotFound():
// cmd/api/healthcheck.go package main import ( "net/http" ) func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) { env := envelope{ "status": "available", "system_info": map[string]string{ "environment": app.config.env, "version": version, }, } err := app.writeJSON(w, http.StatusOK, env, nil) if err != nil { // Используем новый метод для обработки ошибок сервера serverErrorResponse() app.serverErrorResponse(w, r, err) } }
// cmd/api/books.go package main ... func (app *application) showBookHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { // используем новый метод notFoundResponse() app.notFoundResponse(w, r) return } book := data.Book{ ID: id, CreateAt: time.Now(), Title: "Effective Concurrency in Go", Pages: 532, Genres: []string{"IT"}, Edition: 3, } err = app.writeJSON(w, http.StatusOK, envelope{"book": book}, nil) if err != nil { // используем новый метод для обработки ошибок сервера serverErrorResponse() app.serverErrorResponse(w, r, err) } }
Теперь все сообщения об ошибках, отправляемые нашими обработчиками будут представлять собой правильно сформированные ответы в формате JSON.
Может возникнуть вопрос: а что насчёт сообщений об ошибках, которые отправляет chi, когда не может найти соответствующий маршрут? По умолчанию это будут те же ответы в виде обычного текста (без JSON), но, к счастью, chi позволяет настраивать собственные обработчики ошибок при инициализации router.
Эти пользовательские обработчики должны соответствовать интерфейсу http.Handler, поэтому мы можем легко повторно использовать вспомогательные функции notFoundResponse() и methodNotAllowedResponse(), которые только что создали.
Откройте файл cmd/api/routes.go и настройте экземпляр chi следующим образом:
package main import ( "github.com/go-chi/chi/v5" ) func (app *application) routes() *chi.Mux { router := chi.NewRouter() // Преобразуем вспомогательную функцию notFoundResponse() в http.Handler, а затем установим его // в качестве пользовательского обработчика ошибок для ответов 404 Not Found router.NotFound(app.notFoundResponse) // Аналогичным образом преобразуем вспомогательную функцию methodNotAllowedResponse() в http.Handler и установим // её в качестве пользовательского обработчика ошибок для ответов 405 Method Not Allowed router.MethodNotAllowed(app.methodNotAllowedResponse) router.Get("/v1/healthcheck", app.healthcheckHandler) router.Post("/v1/books", app.createBookHandler) router.Get("/v1/books/{id}", app.showBookHandler) return router }
Давайте протестируем изменения. Перезапустите приложение, а затем попробуйте отправить запросы на несуществующие конечные точки или на те, которые используют неподдерживаемый метод HTTP.
Вы должны получить несколько корректных ответов об ошибках в формате JSON, которые выглядят примерно так:
$ curl -i localhost:4000/foo HTTP/1.1 404 Not Found Content-Type: application/json Date: Sun, 07 Dec 2025 17:09:51 GMT Content-Length: 58 { "error": "the requested resource could not be found" } $ curl -i localhost:4000/v1/books/abc HTTP/1.1 404 Not Found Content-Type: application/json Date: Sun, 07 Dec 2025 17:10:57 GMT Content-Length: 58 { "error": "the requested resource could not be found" } $ curl -i -X PUT localhost:4000/v1/healthcheck HTTP/1.1 405 Method Not Allowed Content-Type: application/json Date: Sun, 07 Dec 2025 17:12:12 GMT Content-Length: 66 { "error": "the PUT method is not supported for this resource" }
Хотелось бы отметить, что в некоторых сценариях http.Server Go может автоматически генерировать и отправлять HTTP-ответы в виде обычного текста.
Вот несколько таких сценариев:
В HTTP-запросе указана неподдерживаемая версия протокола HTTP.
HTTP-запрос содержит отсутствующий или недействительный заголовок Host или несколько заголовков Host.
HTTP-запрос содержит недопустимое имя или значение заголовка.
Клиент отправляет HTTP-запрос на HTTPS-сервер.
Например, если мы попытаемся отправить запрос с недопустимым значением заголовка Host, мы получим такой ответ:
$ curl -i -H "Host: Какойтохост" http://localhost:4000/v1/healthcheck HTTP/1.1 400 Bad Request: malformed Host header Content-Type: text/plain; charset=utf-8 Connection: close 400 Bad Request: malformed Host header
Эти ответы жестко запрограммированы в стандартной библиотеке Go, и мы ничего не можем сделать, чтобы настроить их.
До сих пор мы говорили о том, как создавать и отправлять JSON-ответы из API. В следующей статье посмотрим на эту задачу с другой стороны: научимся читать и обрабатывать JSON-запросы от клиента.
Мы продолжим работу с конечной точкой POST /v1/books и обработчиком createBookHandler(), который настроили ранее. Этот эндпоинт должен принимать от клиента данные о новой книге в формате JSON.
Например, запрос на добавление книги Effective Concurrency in Go может выглядеть так:
{ "id": 123, "title": "Effective Сoncurrency in Go", "pages": 532, "genres": [ "IT" ], "edition": 3 }
В следующей части мы сосредоточимся на чтении, разборе и проверке этого тела запроса JSON. В частности, вы узнаете:
Как прочитать тело запроса и преобразовать его в собственный объект Go с помощью пакета encoding/json.
Как обрабатывать некорректные запросы от клиентов и недопустимый JSON, а также возвращать понятные сообщения об ошибках.
Как создать многократно используемый вспомогательный пакет для проверки данных на соответствие нашим бизнес-правилам.
Какие есть методы управления и настройки декодирования JSON.
До встречи!