golang

Приложение на Go шаг за шагом. Часть 4: отправка сообщений об ошибках

  • среда, 11 февраля 2026 г. в 00:00:12
https://habr.com/ru/companies/yandex_praktikum/articles/991602/

Привет! Я Владислав Попов, автор курса «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-запросов

До сих пор мы говорили о том, как создавать и отправлять 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.

До встречи!