Современные курсы стараются максимально охватить спектр технологий, которые используют компании. Ориентироваться в этом океане модных фич всё труднее, особенно это касается новичков, которые только начали знакомство с программированием. В итоге может случиться так, что выпускник курса вроде бы всё знает, а применять не может.
Привет! Я Владислав Попов, автор курса «Go-разработчик с нуля» в Яндекс Практикуме. В серии статей я хочу помочь начинающим разработчикам упорядочить знания и написать приложение на Go с нуля: мы вместе пройдём каждый шаг и создадим API для получения информации о книгах и управления ими.
Серия статей ни в коем случае не претендует на звание универсального руководства по созданию сервисов. Тем не менее я надеюсь, она поможет собрать весь (или почти весь) пазл из полученных знаний.
Предполагается, что вы знакомы с языком программирования Go и он у вас установлен. Также вам потребуются: VS Code с установленным плагином Go, инструмент для работы с HTTP-запросами и ответами в терминале curl, система контроля версий Git и веб-браузер.
Настройка проекта и основная структура папок
Начнём с создания директории проекта. Назовем её itbookworm
. Я создам директорию проекта в $HOME/go/src/github.com/lekan-pvp/itbookworm
, но вы можете сделать это там, где вам нравится:
$ mkdir -p $HOME/go/src/github.com/lekan-pvp/itbookworm
Перейдём в каталог проекта и создадим модуль с помощью go mod init
:
$ cd $HOME/go/src/github.com/lekan-pvp/itbookworm $ go mod init github.com/lekan-pvp/itbookworm go: creating new go.mod: module github.com/lekan-pvp/itbookworm
В итоге мы видим, что был создан файл go.mod
со следующим содержимым (название модуля и версия Go могут отличаться):
module github.com/lekan-pvp/itbookworm go 1.23.2
Итак, каталог проекта и файл go.mod
созданы. Продолжим создавать структуру проекта, запуская следующие команды:
$ mkdir -p bin cmd/api internal migrations remote $ touch Makefile $ touch cmd/api/main.go
На данный момент структура проекта выглядит так:
. |---bin |---cmd | +---api | +---main.go |---internal |---migrations |---remote |---go.mod |---Makefile
Разберёмся, что будет содержать каждый каталог:
-
bin
— скомпилированные двоичные файлы, готовые к развёртыванию на рабочем сервере; -
cmd/api
— основная функция приложения; -
internal
— различные вспомогательные пакеты; -
migrations
— файлы миграции для базы данных; -
remote
— файлы конфигурации и сценарии настроек для производственного сервера; -
go.mod
— информация о зависимостях проекта, версиях и путях к модулям; -
Makefile
— инструкции по автоматизации частых административных задач — проверка кода, создание двоичных файлов и выполнения миграций.
Прежде чем продолжить, проверим, что все наши настройки корректны. Откроем cmd/api/main.go
и добавим код:
// cmd/api/main.go package main import "fmt" func main() { fmt.Println("hello world!") }
Сохраним файл и запустим его:
$ go run ./cmd/api hello world!
Простой HTTP-сервер
Теперь, когда структура проекта готова, сосредоточимся на создании и запуске HTTP-сервера. Сначала будет только один эндпоинт /v1/healthcheck
, который будет возвращать основную информацию об API: текущую версию и производственную среду (разработка, этап, производство; development, stage, production).
Снова откроем cmd/api/main.go
в редакторе и заменим приложение hello world
следующим кодом:
// cmd/api/main.go package main import ( "flag" "fmt" "log" "net/http" "os" "time" ) // Объявим строковую константу, которая содержит номер версии приложения. // Позже мы будем генерировать версию автоматически во время сборки, а пока // просто сохраним жёстко заданную глобальную константу. const version = "1.0.0" // Определим структуру конфигурации, которая будет содержать все параметры // конфигурации для нашего приложения. На данный момент параметрами будут порт, // который должен прослушивать сервер, и название производственной среды (разработка, производство и т.д.). Эти параметры будут считываться из флагов командной строки. type config struct { port int env string } // Определим структуру приложения, которая будет содержать зависимости для // обработчиков HTTP, вспомогательных функций и middleware. На данный момент // она содержит копию структуры конфигурации и логгер. По мере развития проекта // она будет включать в себя гораздо больше. type application struct { config config logger *log.Logger } func main() { // Объявляем экземпляр структуры config. var cfg config // Записываем значения флагов командной строки port и env в структуру // конфигурации. По умолчанию мы используем номер порта 8000 и среду development. flag.IntVar(&cfg.port, "port", 8000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.Parse() // Инициализируем новый логгер, который записывает сообщения в стандартный вывод // с указанием текущей даты и времени. logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) // Объявляем экземпляр структуры приложения, которая содержит структуру // конфигурации и логгер. app := &application{ config: cfg, logger: logger, } // Объявляем новый мультиплексор и добавляем маршрут `/v1/healthcheck`, // который будет перенаправлять запросы в метод `healhcheckHandler` // (мы создадим его чуть позже). mux := http.NewServeMux() mux.HandleFunc("/v1/healthcheck", app.healthcheckHandler) // Объявляем HTTP-сервер с настройками тайм-аута, который прослушивает порт, // указанный в структуре конфигурации, и использует созданный выше мультиплексор. srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: mux, IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } // Запускаем HTTP-сервер logger.Printf("starting %s server on %s", cfg.env, srv.Addr) err := srv.ListenAndServe() logger.Fatal(err) }
Хендлер для проверки работоспособности приложения
Следующее, что нам нужно сделать, — создать метод healthcheckHandler
для обработки HTTP-запросов. Сейчас обработчик будет возвращать простой текст, который будет содержать:
-
Строку
status: available
; -
Версию API из жёстко запрограммированной константы
version
; -
Название среды разработки, которая будет взята из флага командной строки
env
.
Создадим новый файл cmd/api/healthcheck.go
:
$ touch cmd/api/healthcheck.go
И добавим в него код:
// cmd/api/healthcheck.go package main import ( "fmt" "net/http" ) func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "status: available") fmt.Fprintf(w, "environment: %s\n", app.config.env) fmt.Fprintf(w, "version: %s\n", version)
Самое важное в приведённом коде: понимание, что healhcheckHandler
— это метод для структуры application
. Это самый эффективный и общепринятый способ сделать зависимости доступными для обработчиков, не используя глобальные переменные или замыкания.
Чтобы добавить нужную обработчику зависимость, достаточно включить её в структуру приложения. В нашем коде мы применили этот паттерн, когда использовали app.config.env
.
Снова запустите сервер командой go run cmd/app/
и в браузере перейдите по адресу localhost:4000/healthcheck
. Сервер вернёт:
status: available environment: development version: 1.0.0
Или используйте curl
в терминале, где -i
говорит curl
выводить заголовок и тело ответа от сервера:
$ curl -i localhost:4000/v1/healthcheck HTTP/1.1 200 OK Date: Mon, 14 Oct 2024 18:52:38 GMT Content-Length: 58 Content-Type: text/plain; charset=utf-8 status: available environment: development version: 1.0.0
Попробуйте запустить сервер с флагами, которые вы запрограммировали, и другими значениями для них.
Конечные точки API и маршрутизация REST
Теперь мы постепенно будем создавать API, чтобы конечные точки выглядели так:
Метод |
Эндпоинт |
Хендлер |
Действие |
GET |
/v1/healthcheck |
healthcheckHandler |
Показывает информацию о приложении |
GET |
/v1/books| |
listBooksHandler |
Показывает информацию обо всех книгах |
POST |
/v1/books |
createBookHandler |
Создаёт новую книгу |
GET |
v1/books/{id} |
showBookHandler |
Показывает детали определённой книги |
PUT |
/v1/books/{id} |
editBookHandler |
Обновляет информацию определённой книги |
DELETE |
/v1/books/{id} |
deleteBookHandler |
Удаляет определённую книгу |
Выбор роутера
Одним из первых препятствий, с которыми вы столкнётесь при создании API, будет ограниченный функционал маршрутизатора из стандартной библиотеки http.ServeMux
.
Этот роутер не позволяет перенаправлять запросы на разные обработчики в зависимости от методов запроса (GET, POST и т.д.) и не поддерживает чистые URL-адреса со встроенными в путь параметрами.
Например, чтобы получить сведения о конкретной книге, клиент отправляет запрос GET /v1/books/1
вместо того, чтобы добавлять идентификатор книги в качестве параметра строки запроса GET /v1/books?id=1
.
Мы будем использовать популярный маршрутизатор chi. Чтобы его получить, выполним команду:
$ go get github.com/go-chi/chi/v5 go: downloading github.com/go-chi/chi/v5 v5.1.0 go: added github.com/go-chi/chi/v5 v5.1.0
Начнём работу с chi
, добавив в наше приложение два эндпоинта: первый для создания новой книги, второй — для вывода сведений о конкретной книге. К концу этой статьи у нас будет три конечные точки и три обработчика.
Метод |
Эндпоинт |
Хендлер |
Действие |
GET |
/v1/healthcheck |
healthcheckHandler |
Показывает информацию о приложении |
POST |
/v1/books |
createHandler |
Создает новую книгу |
GET |
/v1/books/{id} |
showBookHandler |
Показывает детали определённой книги |
Чтобы функция main()
не стала слишком громоздкой, выделим все правила маршрутизации в отдельный файл cmd/api/routes.go
.
$ touch cmd/api/routes.go
И добавим в него код:
// cmd/api/routes.go package main import ( "github.com/go-chi/chi/v5" ) func (app *application) routes() *chi.Mux { // Инициализируем новый маршрутизатор chi. router := chi.NewRouter() // Регистрируем шаблоны URL и методы обработчиков. router.Get("/v1/healthcheck", app.healthcheckHandler) router.Post("/v1/books", app.createBookHandler) router.Get("/v1/books/{id}", app.showBookHandler) // Возвращаем экземпляр chi.Mux return router }
Обновим функцию main()
, удалив из неё объявление `http.ServeMux`. Используем экземпляр `chi.Mux`, который возвращает метод app.routes()
:
// cmd/api/main.go package main import ( "flag" "fmt" "log" "net/http" "os" "time" ) const version = "1.0.0" type config struct { port int env string } type application struct { config config logger *log.Logger } func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.Parse() logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) app := &application { config: cfg, logger: logger, } srv := &http.Server { Addr: fmt.Sprintf(":%d",cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } logger.Printf("starting %s server on %s", cfg.env, srv.Addr) err := srv.ListenAndServe() logger.Fatal(err) }
Добавление новых функций-обработчиков
Теперь, когда правила маршрутизации настроены, мы можем создать методы createBookHandler
и showBookHandler
для новых эндпоинтов. Метод showBookHandler
представляет некоторый интерес, потому что в нём мы будем извлекать параметр id
из URL-адреса и использовать его в HTTP-ответе.
Создадим файл cmd/api/books.go
:
touch cmd/api/books.go
И добавим в него код:
// cmd/api/books.go package main import ( "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" ) // Добавляем обработчик createBookHandler для эндпоинта `POST /v1/books`. // Пока мы просто возвращаем текст. func (app *application) createBookHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "create a new book") } // Добавляем обработчик showBookHandler для конечной точки `GET /v1/books/:id`. // На данный момент мы получаем параметр id из URL-адреса и включаем его в ответ. func (app *application) showBookHandler(w http.ResponseWriter, r *http.Request) { // Мы можем использовать функцию chi.URLParam(), чтобы получить параметр id из URL. // Первый параметр в функции — это http.Request. param := chi.URLParam(r, "id") // В нашем проекте все книги будут иметь уникальный положительный идентификатор, // но возвращаемое значение метода URLParam() всегда является строкой, // поэтому мы пытаемся преобразовать её в целое число. Если не удаётся // её преобразовать или id меньше 1, то идентификатор является недействительным, и мы // вызываем http.NotFound() для возврата ошибки 404. id, err := strconv.ParseInt(param, 10, 64) if err != nil || id < 1 { http.NotFound(w, r) return } // Если идентификатор валидный, мы возвращаем его в ответе. fmt.Fprintf(w, "show the details of book %d\n", id) }
Давайте проверим, как всё работает. Перезапустите приложение:
$ go run ./cmd/api 2024/10/21 17:13:52 starting development server on :4000
Пока сервер запущен, откройте другой терминал и введите запросы для каждого обработчика с помощью команды curl
:
$ curl localhost:4000/v1/healthcheck status: available environment: development version: 1.0.0 $ curl -X POST localhost:4000/v1/books create a new book $ curl localhost:4000/v1/books/12 show the details of book 12
Обратите внимание, что в последнем запросе значение параметра 12 было успешно получено из URL и включено в ответ.
А что, если сделать запрос с неподдерживаемым методом:
$ curl -i -X POST localhost:4000/v1/healthcheck HTTP/1.1 405 Method Not Allowed Allow: GET, OPTIONS Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Mon, 21 Oct 2024 14:28:52 GMT Content-Length: 19 Method Not Allowed
Получили ошибку 405 Method Not Allowed
— метод не поддерживается.
А теперь попробуем передать в запросе отрицательный id
, чтобы получить ошибку 404 Not Found
:
$ curl -i localhost:4000/v1/books/-3 HTTP/1.1 404 Not Found Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff Date: Mon, 21 Oct 2024 14:37:01 GMT Content-Length: 19 404 page not found
В качестве самостоятельной работы сделайте запрос, где id будет строкой, которую невозможно преобразовать в целое положительное число. Обратите внимание на ошибку.
Создание помощника для чтения параметра id
Код для извлечения параметра id
из URL, например, из /v1/books/{id}
, понадобится нам неоднократно. Поэтому давайте вынесем эту логику в небольшой вспомогательный метод, который можно будет использовать повторно.
Создадим новый файл cmd/api/helpers.go
:
$ touch cmd/api/helpers.go
И добавим новый метод для структуры application
:
// cmd/api/helpers.go package main import ( "errors" "net/http" "strconv" "github.com/go-chi/chi/v5" ) // readIDParam получает параметр id из контекста запроса, преобразует его в // целое число и возвращает его. Если операция не удалась, возвращает 0 и сообщение // об ошибке. func (app *application) readIDParam(r *http.Request) (int64, error) { param := chi.URLParam(r, "id") id, err := strconv.ParseInt(param, 10, 64) if err != nil || id < 1 { return 0, errors.New("invalid parameter") } return id, nil }
С помощью нового метода readIDParam()
упростим код в обработчике showBookHandler()
:
// cmd/api/books.go package main // ... func (app *application) showBookHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { http.NotFound(w, r) return } fmt.Fprintf(w, "show the details of book %d\n", id) }
На этом первая часть завершена. На данном этапе мы уже создали сервер и добавили три обработчика для трёх эндпоинтов. Мы также добавили простенькую конфигурацию и логгер, которые впоследствии будем развивать.
В следующей части начнём работать с JSON. Мы обновим наши обработчики, чтобы он возвращали JSON вместо обычного текста.
ссылка на оригинал статьи https://habr.com/ru/articles/854482/
Добавить комментарий