Привет! Я Владислав Попов, автор курса «Go-разработчик с нуля» в Яндекс Практикуме. В серии статей я хочу помочь начинающим разработчикам упорядочить знания и написать приложение на Go с нуля: мы вместе пройдём каждый шаг и создадим API для получения информации о книгах и управления ими.
В прошлых частях мы создали сервер с тремя эндпоинтами, добавили базовую конфигурацию, логгер и обновили обработчики для возврата ответов в формате JSON. В этой статье будем продолжать развивать наш сервис и узнаем про более продвинутые способы работы с JSON.
Улучшение читаемости ответов
До сих пор, когда мы делали запросы с помощью curl, мы получали в ответ JSON в виде одной строки без пробелов и переноса строк.
$ curl localhost:4000/v1/healthcheck {"environment":"development","status":"available","version":"1.0.0"} $ curl localhost:4000/v1/books/123 {"id":123,"title":"Effective Concurrency in Go","pages":532,"genres":["IT"],"edition":3}
Мы можем сделать вывод более читабельным, используя функцию json.MarshalIndent() (подробнее о MarshalIndent — в документации JSON) вместо json.Marshal(). Давайте обновим функцию writeJSON():
// cmd/api/helpers.go package main func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, headers http.Header) error { // Используем функцию json.MarshalIndent() для добавления переноса строк в закодированный JSON. Мы не используем префикс во втором параметре и используем табуляцию для отступов - в третьем. js, err := json.MarshalIndent(data, "", "\t") if err != nil { return err } js = append(js, '\n') for key, value := range headers { w.Header()[key] = value } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(js) return nil }
Если перезапустить сервер и сделать несколько запросов из терминала, вы сможете увидеть, что ответы теперь читать проще.
$ curl localhost:4000/v1/healthcheck { "environment": "development", "status": "available", "version": "1.0.0" } $ curl localhost:4000/v1/books/123 { "id": 123, "title": "Effective Concurrency in Go", "pages": 532, "genres": [ "IT" ], "edition": 3 }
Стоит обратить внимание, что, хотя читабельность ответов улучшилась, чем-то пришлось пожертвовать. Во-первых, ответы теперь стали немного больше по объему. Во-вторых, теперь Go выполняет дополнительную работу, чтобы расставить пробелы, что в свою очередь снижает производительность.
Давайте теперь обновим наши ответы, чтобы они были полноценным JSON-объектом. Вот так:
{ "book": { "id": 123, "title": "Effective Concurrency in Go", "pages": 532, "genres": [ "IT" ], "edition": 3 } }
Обратите внимание, что данные книги теперь вложены в ключ "book", а не являются объектом верхнего уровня.
Такое оформление ответа не является обязательным, но оно имеет ряд преимуществ:
-
Включение ключевого имени (например,
"book") на верхнем уровне JSON помогает сделать ответ более самодокументируемым. Людям, которые увидят ответ вне контекста, будет немного проще понять, к чему относятся данные. -
Это снижает риск ошибок на стороне клиента. Чтобы получить данные, клиент должен явно указать на них с помощью ключа
"book". -
Если мы всегда будем упаковывать данные, возвращаемые API, то устраним уязвимость в системе безопасности в старых браузерах, которая может возникнуть, если возвращать массив JSON в качестве ответа.
Есть несколько способов, которые мы могли бы использовать для упаковки ответов API, но будем придерживаться простоты и сделаем это с помощью мапы map[string]interface{}.
Давайте обновим cmd/api/helpers.go:
// cmd/api/helpers.go package main type envelope map[string]interface{} func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { js, err := json.MarshalIndent(data, "", "\t") if err != nil { return err } js = append(js, '\n') for key, value := range headers { w.Header()[key] = value } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(js) return nil }
Теперь нужно обновить showBookHandler(), чтобы создать экземпляр мапы, содержащей данные о книге, и передать его методу writeJSON(), вместо передачи данных напрямую.
// cmd/api/books.go func (app *application) showBookHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { http.NotFound(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 { app.logger.Println(err) http.Error(w, "Сервер обнаружил проблему и не смог обработать ваш запрос", http.StatusInternalServerError) } }
Нам также нужно обновить код в healthcheckHandler(), так как он тоже использует writeJSON().
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) { // Объявите мапу, содержащую данные для ответа. Обратите внимание, что способ, которым // мы это сделали, означает, что данные о среде выполнения и версии теперь будут вложены // в ключ system_info в ответе JSON. 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 { app.logger.Panicln(err) http.Error(w, "Сервер обнаружил проблему и не смог обработать ваш запрос", http.StatusInternalServerError) } }
Давайте проверим обновления. Перезапустите сервер и сделайте запрос:
$ curl localhost:4000/v1/books/123 { "book": { "id": 123, "title": "Effective Concurrency in Go", "pages": 532, "genres": [ "IT" ], "edition": 3 } } $ curl localhost:4000/v1/healthcheck { "status": "available", "system_info": { "environment": "development", "version": "1.0.0" } }
Продвинутая кастомизация JSON
Мы уже смогли расширить возможности настройки JSON-ответов с помощью тегов структуры, красивого форматирования и оборачивания данных. Прежде чем продолжить, давайте сначала разберёмся, как Go кодирует JSON за кулисами.
Прежде чем кодировать определённый тип в JSON, Go проверяет, реализован ли для этого типа метод MarshalJSON(). Если реализован, Go вызывает этот метод. Строго говоря, когда Go кодирует определённый тип в JSON, он проверяет, реализует ли этот тип интерфейс json.Marshaler. Вот он:
type Marshaler interface { MarshalJSON() ([]byte, error) }
Если тип удовлетворяет интерфейсу, то Go вызывает для этого типа метод MarshalJSON() и использует возвращаемый массив []byte в качестве закодированного значения JSON.
Если у типа нет метода MarshalJSON(), то Go пытается закодировать JSON на основе собственного набора правил.
Таким образом, если мы хотим изменить способ кодирования JSON для какого-либо типа, всё, что нам нужно, — это реализовать для него метод MarshalJSON(), который возвращает слайс байт []byte в JSON-представлении.
Давайте сделаем кастомизацию для поля Pages в нашей структуре Book.
Кастомизация поля Pages
Когда наша структура Book кодируется в JSON, поле Pages(типа int32) кодируется как просто целое число. Давайте сделаем так, чтобы оно кодировалось как строка <количество> страниц.
{ "book": { "id": 123, "title": "Effective Concurrency in Go", "pages": "532 pages", "genres": [ "IT" ], "edition": 3 } }
Есть несколько способов добиться этого, но самый простой — создать пользовательский тип для поля Pages и реализовать для него метод MarshalJSON().
Давайте создадим новый файл internal/data/pages.go для хранения логики типа Pages:
package data import ( "fmt" "strconv" ) // Объявляем пользовательский тип Pages, который имеет базовый тип int32 // такой же, как был у поля Pages структуры Book type Pages int32 // Реализуем метод MarshalJSON(), чтобы тип Pages удовлетворял интерфейсу json.Marshaler // Метод должен возвращать количество страниц в книге в кодировке JSON в таком виде: // "<количество> страниц" func (p Pages) MarshalJSON() ([]byte, error) { // Создаем строку, которая содержит количество страниц в нужном нам формате jsonValue := fmt.Sprintf("%d pages", p) // Используем функцию strconv.Quote() для созданной строки, чтобы заключить её в // двойные кавычки. Это нужно, так как строки в JSON заключены в кавычки. quotedJSONValue := strconv.Quote(jsonValue) // Преобразовываем строку в массив байтов и возвращаем её return []byte(quotedJSONValue), nil }
Следует обратить внимание на пару вещей:
1. Если метод MarshalJSON() возвращает значение в виде строки JSON, как у нас, то перед тем как вернуть строку, нужно заключить её в двойные кавычки. Если этого не сделать, она не будет интерпретироваться как строка JSON, и вы получите ошибку.
2. Мы намеренно делаем приёмник, не указатель на тип, как func (p *Pages) MarshalJSON(). Это даёт больше гибкости, так как в этом случае метод сможет работать как со значениями Pages, так и с указателями на Pages.
Теперь, когда метод MarshalJSON() для нашего типа Pages определён, откройте файл internal/data/books.go и обновите структуру Book:
package data import "time" type Book struct { ID int64 `json:"id"` CreateAt time.Time `json:"-"` Title string `json:"title"` Year int32 `json:"year,omitempty"` // Используйте тип Pages вместо int32. Обратите внимание, что omitempty // по-прежнему будет работать, и если поле Pages будет равно 0, то оно будет считаться пустым // и не будет включено в JSON, а метод MarshalJSON(), который мы создали, // вообще не будет вызван. Pages Pages `json:"pages,omitempty"` Genres []string `json:"genres,omitempty"` Edition int32 `json:"edition"` }
Давайте попробуем перезапустить сервер и сделать запрос:
$ curl localhost:4000/v1/books/123 { "book": { "id": 123, "title": "Effective Concurrency in Go", "pages": "532 pages", "genres": [ "IT" ], "edition": 3 } }
Вы должны увидеть такой ответ. Обратите внимание на то, как представлены страницы в ответе.
В общем, это довольно удобный способ создания пользовательского JSON. Наш код лаконичен и понятен, и у нас есть собственный тип Pages, который мы можем использовать везде и всегда, когда нам это нужно.
Но есть и обратная сторона. Важно знать, что использование пользовательских типов иногда может приводить к затруднениям при интеграции вашего кода с другими пакетами. Вам может потребоваться выполнить преобразование типов, чтобы изменить пользовательский тип на значение, которое понимают и принимают другие пакеты.
На этом третья часть закончена. В ней вы узнали о продвинутой работе с JSON и реализовали метод MarshalJSON(), чтобы кастомизировать один из ключей JSON. На данный момент API отправляет хорошо отформатированные JSON-ответы на успешные запросы.
Если клиент отправляет неверный запрос или что-то идёт не так в нашем приложении, мы по-прежнему отправляем ему текстовое сообщение об ошибке с помощью функций http.Error() и http.NotFound().
В следующей части мы исправим это и добавим несколько помощников для обработки ошибок и отправки соответствующих ответов клиентам.
ссылка на оригинал статьи https://habr.com/ru/articles/927524/
Добавить комментарий