{"id":34955937,"url":"https://github.com/stevenstr/gh-actions-sbs","last_synced_at":"2026-05-19T21:33:31.248Z","repository":{"id":304995691,"uuid":"1021527982","full_name":"stevenstr/gh-actions-sbs","owner":"stevenstr","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-30T15:44:13.000Z","size":142,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-28T10:08:08.874Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevenstr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-17T14:30:48.000Z","updated_at":"2025-07-30T15:44:17.000Z","dependencies_parsed_at":"2025-07-17T21:31:00.226Z","dependency_job_id":null,"html_url":"https://github.com/stevenstr/gh-actions-sbs","commit_stats":null,"previous_names":["stevenstr/gh-actions-sbs"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stevenstr/gh-actions-sbs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenstr%2Fgh-actions-sbs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenstr%2Fgh-actions-sbs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenstr%2Fgh-actions-sbs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenstr%2Fgh-actions-sbs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevenstr","download_url":"https://codeload.github.com/stevenstr/gh-actions-sbs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenstr%2Fgh-actions-sbs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33233740,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-19T15:49:41.270Z","status":"ssl_error","status_checked_at":"2026-05-19T15:49:22.917Z","response_time":58,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-26T22:10:33.594Z","updated_at":"2026-05-19T21:33:31.232Z","avatar_url":"https://github.com/stevenstr.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://github.com/stevenstr/gh-actions-sbs/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/stevenstr/gh-actions-sbs/actions/workflows/ci.yaml)\n\n# gh-actions-sbs\n\n# Go REST API Project\n\n## Описание проекта\n\nПроект представляет собой простой REST API, написанный на языке программирования Go. Основная цель проекта — демонстрация базовых принципов создания REST API, интеграции документации с помощью Swagger, контейнеризации с использованием Docker и автоматизации процессов сборки и тестирования с помощью GitHub Actions.\n\n## Функции проекта\n\n1. **REST API**:\n   - Два хендлера: `/hello` и `/goodbye`, которые возвращают JSON-ответы с текстом \"Hello, World!\" и \"Goodbye, World!\" соответственно.\n   - Использование фреймворка Gin для создания маршрутов и обработки запросов.\n\n2. **Документация с помощью Swagger**:\n   - Автоматическая генерация документации API в формате OpenAPI.\n   - Доступ к документации через маршрут `/swagger/index.html`.\n\n3. **Контейнеризация с Docker**:\n   - Создание Dockerfile для сборки и запуска приложения в контейнере.\n   - Упрощение развертывания и обеспечение консистентности окружения.\n\n4. **Автоматизация с GitHub Actions**:\n   - Настройка CI/CD pipeline для автоматической сборки, тестирования и запуска приложения при каждом пуше в ветку `main` или при создании pull request.\n   - Интеграция с Docker для выполнения тестов и запуска приложения в контейнере.\n\n## Технологии\n\n- **Go**: Язык программирования для создания REST API.\n- **Gin**: Веб-фреймворк для Go, используемый для создания маршрутов и обработки запросов.\n- **Swagger**: Инструмент для генерации документации API в формате OpenAPI.\n- **Docker**: Платформа для контейнеризации приложений.\n- **GitHub Actions**: Инструмент для автоматизации процессов CI/CD.\n\n## Механизмы\n\n1. **Создание REST API**:\n   - Определение маршрутов и хендлеров с использованием Gin.\n   - Обработка HTTP-запросов и возвращение JSON-ответов.\n\n2. **Генерация документации**:\n   - Использование аннотаций для описания маршрутов и параметров.\n   - Генерация файла `swagger.json` с помощью команды `swag init`.\n\n3. **Контейнеризация**:\n   - Создание Dockerfile для сборки приложения.\n   - Установка зависимостей и сборка приложения в контейнере.\n   - Запуск контейнера с приложением.\n\n4. **Автоматизация CI/CD**:\n   - Настройка GitHub Actions для автоматической сборки и тестирования.\n   - Использование Docker для выполнения тестов и запуска приложения в контейнере.\n   - Автоматическое выполнение workflow при пуше в ветку `main` или при создании pull request.\n\n## Базовая структура после выполгнения частей 1-3\ngo-rest-api/\n├── Dockerfile\n├── go.mod\n├── go.sum\n├── main.go\n├── docs/\n│   ├── docs.go\n│   └── swagger.json\n├── tests/\n│   └── main_test.go\n└── .github/\n└── workflows/\n└── ci.yml\n\n## Базовая структура после выполнения части 4\ngo-rest-api/\n├── cmd/\n│   └── main.go\n├── internal/\n│   ├── api/\n│   │   └── handler.go\n│   ├── config/\n│   │   └── config.go\n│   └── swagger/\n│       └── docs.go\n├── pkg/\n│   └── utils/\n│       └── utils.go\n├── docs/\n│   └── swagger.json\n├── tests/\n│   └── main_test.go\n├── Dockerfile\n├── go.mod\n├── go.sum\n└── .github/\n    └── workflows/\n        └── ci.yml\n\n\n## Установка и запуск\n\n1. **Клонируйте репозиторий**:\n```sh\ngit clone https://github.com/yourusername/go-rest-api.git\ncd go-rest-api\n```\n\n2. **Установите зависимости:\n```sh\n\ngo mod tidy\n```\n\n3. **Соберите и запустите приложение:\n```sh\n\ngo run main.go\n```\n\n4. **Соберите Docker образ и запустите контейнер:\n```sh\n\ndocker build -t go-rest-api .\ndocker run --rm -p 8080:8080 go-rest-api\n```\n\n5. **Откройте браузер и перейдите по адресам:\nhttp://localhost:8080/hello\nhttp://localhost:8080/goodbye\nhttp://localhost:8080/swagger/index.html (для документации Swagger)\n\n# Лицензия\nЭтот проект лицензирован под лицензией MIT.\n\n\n\n# Часть 01\nДавайте создадим простой REST API на Go с парой хендлеров и без базы данных. Затем мы добавим минимальный набор тестов и настроим GitHub Actions для CI/CD без пуша в Docker Hub.\n\n## Шаг 1: Создание проекта\nСоздайте новую директорию для вашего проекта и перейдите в нее:\n```sh\nmkdir go-rest-api\ncd go-rest-api\n```\n\nИнициализируйте новый модуль Go:\n```sh\ngo mod init go-rest-api\n```\n\n## Шаг 2: Создание REST API\n\nСоздайте файл main.go и добавьте следующий код:\n```go\npackage main\n\nimport (\n    \"encoding/json\"\n    \"net/http\"\n)\n\ntype Message struct {\n    Text string `json:\"text\"`\n}\n\nfunc helloHandler(w http.ResponseWriter, r *http.Request) {\n    message := Message{Text: \"Hello, World!\"}\n    json.NewEncoder(w).Encode(message)\n}\n\nfunc goodbyeHandler(w http.ResponseWriter, r *http.Request) {\n    message := Message{Text: \"Goodbye, World!\"}\n    json.NewEncoder(w).Encode(message)\n}\n\nfunc main() {\n    http.HandleFunc(\"/hello\", helloHandler)\n    http.HandleFunc(\"/goodbye\", goodbyeHandler)\n\n    http.ListenAndServe(\":8080\", nil)\n}\n```\n\n\n## Шаг 3: Написание тестов\nСоздайте директорию файл main_test.go:\n```sh\n\n main_test.go\n```\n\nУстанови пакет:\n```sh\ngo get github.com/stretchr/testify/assert\n```\n\nДобавьте следующий код в tests/main_test.go:\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHelloHandler(t *testing.T) {\n\treq, err := http.NewRequest(\"GET\", \"/hello\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trr := httptest.NewRecorder()\n\thandler := http.HandlerFunc(HelloHandler)\n\thandler.ServeHTTP(rr, req)\n\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.JSONEq(t, `{\"text\":\"Hello GH Actions World!\"}`, rr.Body.String())\n}\n\nfunc TestGoodbyeHandler(t *testing.T) {\n\treq, err := http.NewRequest(\"GET\", \"/goodbye\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trr := httptest.NewRecorder()\n\thandler := http.HandlerFunc(GoodbyeHandler)\n\thandler.ServeHTTP(rr, req)\n\n\tassert.Equal(t, http.StatusOK, rr.Code)\n\tassert.JSONEq(t, `{\"text\":\"Goodbye GH Actions World!\"}`, rr.Body.String())\n}\n\n```\n\n\n## Шаг 4: Настройка GitHub Actions\n\nДавай разложим по полочкам основные термины в CI/CD и Docker, чтобы всё стало понятно и логично.\n\n🔄 CI/CD: Непрерывная интеграция и доставка\nТермин\tОбъяснение\nCI (Continuous Integration)\tНепрерывная интеграция — автоматическая проверка и сборка кода при каждом коммите. Цель — быстро находить ошибки.\nCD (Continuous Delivery)\tНепрерывная доставка — автоматическая подготовка приложения к выкладке в продакшн. Вы сами решаете, когда выкладывать.\nCD (Continuous Deployment)\tНепрерывное развертывание — автоматическая выкладка в прод после успешной сборки и тестов. Всё без участия человека.\nPipeline\tПоследовательность шагов: сборка, тестирование, деплой. Описывается в конфигурационном файле (например, .gitlab-ci.yml).\nStage / Job\tЭтапы и задачи внутри pipeline. Например: build, test, deploy.\nRunner / Agent\tСервер или контейнер, который выполняет pipeline. Например, GitLab Runner.\nArtifact\tФайл или результат сборки, который передаётся между этапами. Например, бинарник Go.\nTrigger\tСобытие, запускающее pipeline: коммит, merge request, установка тега.\n\nСоздайте директорию .github/workflows и файл ci.yml внутри нее:\n```sh\n\nmkdir -p .github/workflows\ntouch .github/workflows/ci.yml\n```\n\n### Общая структура\nФайл .github/workflows/ci.yml является конфигурационным файлом для GitHub Actions. Он определяет, как и когда должны выполняться автоматические задачи (workflows) в вашем репозитории. В данном случае, это CI (Continuous Integration) pipeline, который будет автоматически запускаться при определенных событиях.\n\nЭтот файл автоматизирует процесс сборки и тестирования вашего проекта. Когда вы делаете пуш в ветку main или создаете pull request в эту ветку, GitHub Actions автоматически:\n\nКлонирует ваш репозиторий.\nУстанавливает Go.\nКомпилирует ваш проект.\nЗапускает все тесты.\nЭто помогает убедиться, что ваш код работает корректно и не содержит ошибок перед тем, как он будет объединен в основную ветку.\n\nДобавьте следующий код в ci.yml:\n```yaml\nname: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: '1.18'\n\n    - name: Build\n      run: go build -v ./...\n\n    - name: Test\n      run: go test -v ./...\n```\n\n\n### Разбор файла\n```yaml\nname: CI\n```\nname: Это имя вашего workflow. В данном случае, оно называется \"CI\", что означает Continuous Integration.\n\n```yaml\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n```\non: Определяет события, при которых будет запускаться этот workflow.\n\npush: Workflow будет запускаться при каждом пуше в ветку main.\n\npull_request: Workflow будет запускаться при каждом создании pull request в ветку main.\n\n\n```yaml\njobs:\n  build:\n    runs-on: ubuntu-latest\n```\njobs: Определяет набор задач (jobs), которые будут выполняться в этом workflow.\n\nbuild: Имя задачи. В данном случае, задача называется \"build\".\n\nruns-on: Определяет операционную систему, на которой будет выполняться задача. В данном случае, это ubuntu-latest, что означает последнюю версию Ubuntu.\n\n\n```yaml\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n```\nsteps: Определяет последовательность шагов, которые будут выполняться в этой задаче.\n\nname: Описание шага.\n\nuses: Указывает, какой action использовать. В данном случае, это actions/checkout@v2, который клонирует репозиторий в рабочую директорию.\n\n\n```yaml\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: '1.18'\n```\nname: Описание шага.\n\nuses: Указывает, какой action использовать. В данном случае, это actions/setup-go@v2, который устанавливает Go на рабочую машину.\n\nwith: Передает параметры в action. В данном случае, устанавливается версия Go 1.18.\n\n```yaml\n    - name: Build\n      run: go build -v ./...\n```\nname: Описание шага.\n\nrun: Команда, которая будет выполнена на этом шаге. В данном случае, это go build -v ./..., которая компилирует все пакеты в проекте.\n\n\n```yaml\n    - name: Test\n      run: go test -v ./...\n```\nname: Описание шага.\n\nrun: Команда, которая будет выполнена на этом шаге. В данном случае, это go test -v ./..., которая запускает все тесты в проекте.\n\n\n\n\n## Шаг 5: Запуск и тестирование\nЗапустите ваше приложение локально:\n```sh\ngo run main.go\n```\n\nОткройте браузер и перейдите по адресам http://localhost:8080/hello и http://localhost:8080/goodbye, чтобы убедиться, что API работает.\n\n\nЗапустите тесты локально:\n```sh\n\ngo test -v ./...\n```\n\n## Шаг 6: Запуск GitHub Actions\nСоздайте репозиторий на GitHub и загрузите туда ваш проект.\nGitHub Actions автоматически запустит CI pipeline при каждом пуше в ветку main или при создании  pull request в эту ветку. Вы можете следить за прогрессом в разделе \"Actions\" вашего репозитория на GitHub.\n\n## Шаг 7: Дополнительные улучшения (опционально)\nЕсли вы хотите добавить дополнительные улучшения, такие как логгирование, middleware или более сложные тесты, вы можете сделать это следующим образом:\n\n### Логгирование:\nДобавьте пакет log/slog для логгирования запросов и ответов.\n```go\nimport (\n    \"log\"\n    \"net/http\"\n)\n\nfunc helloHandler(w http.ResponseWriter, r *http.Request) {\n    log.Println(\"Received request at /hello\")\n    message := Message{Text: \"Hello, World!\"}\n    json.NewEncoder(w).Encode(message)\n}\n\nfunc goodbyeHandler(w http.ResponseWriter, r *http.Request) {\n    log.Println(\"Received request at /goodbye\")\n    message := Message{Text: \"Goodbye, World!\"}\n    json.NewEncoder(w).Encode(message)\n}\n```\n\n### Middleware:\nСоздайте middleware для логгирования времени выполнения запросов.\n```go\nfunc loggingMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        start := time.Now()\n        next.ServeHTTP(w, r)\n        log.Printf(\"Method: %s, Path: %s, Duration: %v\", r.Method, r.URL.Path, time.Since(start))\n    })\n}\n\nfunc main() {\n    http.Handle(\"/hello\", loggingMiddleware(http.HandlerFunc(helloHandler)))\n    http.Handle(\"/goodbye\", loggingMiddleware(http.HandlerFunc(goodbyeHandler)))\n\n    http.ListenAndServe(\":8080\", nil)\n}\n```\n\n\n## Более сложные тесты:\nДобавьте тесты для проверки middleware и других аспектов вашего приложения.\n```go\nfunc TestLoggingMiddleware(t *testing.T) {\n    req, err := http.NewRequest(\"GET\", \"/hello\", nil)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    rr := httptest.NewRecorder()\n    handler := loggingMiddleware(http.HandlerFunc(helloHandler))\n    handler.ServeHTTP(rr, req)\n\n    assert.Equal(t, http.StatusOK, rr.Code)\n    assert.Equal(t, `{\"text\":\"Hello, World!\"}`, rr.Body.String())\n}\n```\n\n\n## Заключение\nТеперь у вас есть простой REST API на Go с парой хендлеров, минимальным набором тестов и настроенным GitHub Actions для CI/CD. Вы можете расширять этот пример, добавляя больше функциональности, тестов и улучшений по мере необходимости.\n\n\n\n# Часть 2\nдавай добавим к этому проекту swagger\n\nSwagger (OpenAPI) — это мощный инструмент для документации API. Давайте добавим Swagger в наш проект на Go. Мы будем использовать библиотеку swaggo и swaggo/swag для генерации документации Swagger.\n\n## Шаг 1: Установка зависимостей\nУстановите необходимые пакеты:\n```sh\ngo get -u github.com/swaggo/swag/cmd/swag\ngo get -u github.com/swaggo/gin-swagger\ngo get -u github.com/swaggo/files\n```\n\n```sh\nexport PATH=$PATH:$(go env GOPATH)/bin\n```\n\n\n# Шаг 2: Настройка Swagger\n\nАвтодок-комментарии (auto-generated documentation comments) — это специальные комментарии в коде, которые расставляются вручную вами, но по заранее определённому шаблону (синтаксису), понятному инструменту генерации документации (в нашем случае — утилите swag).\n\nКак это работает:\n\nВы пишете над функцией-хендлером (или над пакетом) «мета-комментарии», начинающиеся с // @….\nУтилита swag (команда swag init) сканирует ваш исходный код, собирает эти // @…-строки и превращает их в файл docs/swagger.json (или swagger.yaml) в формате OpenAPI/Swagger.\nSwagger UI (к которому вы подключаетеся через gin-swagger.WrapHandler) читает этот JSON/YAML и отображает документацию в браузере.\nПример автодок-комментариев в коде (взятый из предыдущего ответа):\n\n```go\n\n// @Summary      Hello World\n// @Description  Returns a hello message\n// @ID           helloHandler\n// @Produce      json\n// @Success      200  {object}  Message\n// @Router       /hello [get]\nfunc helloHandler(c *gin.Context) { … }\n```\nРасшифровка основных меток (@ tags):\n\n@Summary — краткое описание метода.\n@Description — подробное описание.\n@ID — уникальный идентификатор операции (используется внутри Swagger).\n@Accept / @Produce — форматы входящих/исходящих данных (json, xml и т. д.).\n@Param — описание параметров (query, body, path и пр.).\n@Success / @Failure — описание возможных ответов (код, тип, модель данных).\n@Router — путь и HTTP-метод в формате \u003cpath\u003e [\u003cmethod\u003e].\nПлюс общие метки над пакетом/файлом: @title, @version, @host, @BasePath и пр.\nОткуда «высрать» шаблон автодок-комментариев?\n\nОфициальная документация swaggo:\nhttps://github.com/swaggo/swag#declarative-comments-format\nПримеры в репозитории gin-swagger:\nhttps://github.com/swaggo/gin-swagger#usage\nВ сорцах проектов, где уже используется swag: посмотрите, как оформлены // @… в популярных Go-репозиториях.\nКак начать:\na) Ставите swag в PATH.\nb) В корне проекта запускаете swag init — он автоматически создаст /docs.\nc) Дописываете // @…-комментарии к вашим хендлерам (и к корню пакета, чтобы задать общие параметры API).\nd) Перезапускаете swag init, обновляете /docs/swagger.json.\ne) Открываете UI по /swagger/index.html.\n\nТаким образом «автодок-комментарии» — это просто ваша разметка исходников под генератор Swagger-документации.\n\n\nОпишем апи в main.go\n```go\n// main.go\npackage main\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t_ \"github.com/stevenstr/gh-actions-sbs/docs\" // docs генерится swag-ом\n\tswaggerFiles \"github.com/swaggo/files\"\n\tginSwagger \"github.com/swaggo/gin-swagger\"\n)\n\n// Message — структура ответа\ntype Message struct {\n\tText string `json:\"text\"`\n}\n\n// @title           Simple API\n// @version         1.0\n// @description     This is a sample server.\n// @termsOfService  http://example.com/terms/\n\n// @contact.name   API Support\n// @contact.url    http://www.example.com/support\n// @contact.email  support@example.com\n\n// @license.name  Apache 2.0\n// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html\n\n// @host      localhost:8080\n// @BasePath  /\n\n// @Summary      Hello World\n// @Description  Returns a hello message\n// @ID           helloHandler\n// @Produce      json\n// @Success      200  {object}  Message\n// @Router       /hello [get]\nfunc helloHandler(c *gin.Context) {\n\tc.JSON(http.StatusOK, Message{Text: \"Hello, World!\"})\n}\n\n// @Summary      Goodbye World\n// @Description  Returns a goodbye message\n// @ID           goodbyeHandler\n// @Produce      json\n// @Success      200  {object}  Message\n// @Router       /goodbye [get]\nfunc goodbyeHandler(c *gin.Context) {\n\tc.JSON(http.StatusOK, Message{Text: \"Goodbye, World!\"})\n}\n\nfunc main() {\n\tr := gin.Default()\n\n\tr.GET(\"/hello\", helloHandler)\n\tr.GET(\"/goodbye\", goodbyeHandler)\n\n\t// Роут для Swagger UI\n\tr.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\n\tr.Run(\":8080\")\n}\n```\n\n\n## Шаг 3: Генерация документации\nСгенерируйте документацию Swagger:\n```sh\n\nswag init\n```\nЭта команда создаст файл docs/swagger.json, который содержит документацию API в формате OpenAPI.\n\n# Шаг 4: Запуск и проверка\nЗапустите ваше приложение:\n```sh\n\ngo run main.go\n```\n\nОткройте браузер и перейдите по адресу http://localhost:8080/swagger/index.html, чтобы увидеть документацию Swagger.\n\n\n\n# Шаг 5: Запуск и проверка\nНиже разберём пример «старта» HTTP-сервера с возможностью плавного (graceful) завершения по шагам.\n\n```go\nimport (\n    \"context\"\n    \"log\"\n    \"net/http\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n    \"time\"\n    \"github.com/gin-gonic/gin\"\n)\n```\n\n– Пакет net/http и github.com/gin-gonic/gin для запуска HTTP-сервера.\n– context, os/signal, syscall, time – для организации graceful-shutdown.\n– log – для логирования.\n\n```go\nfunc main() {\n  // 1. Инициализируем Gin с дефолтными middleware (Logger, Recovery)\n\trouter := gin.Default()\n\n   // 2. Регистрируем любые эндпоинты\n\trouter.GET(\"/hello\", helloHandler)\n\trouter.GET(\"/goodbye\", goodbyeHandler)\n\n\t// Роут для Swagger UI\n\trouter.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\n  // 3. Оборачиваем router в http.Server\n//   – Оборачиваем gin.Engine (реализующий http.Handler) в стандартный http.Server.\n// – Благодаря этому можем управлять запуском и завершением более тонко, чем вызывая просто r.Run().\n\tsrv := \u0026http.Server{\n\t\tAddr:    \":8080\",\n\t\tHandler: router,\n\t}\n \n\n\t// 4. Запускаем сервер в отдельной горутине\n//   – Запускаем ListenAndServe() в новой горутине, чтобы основная программа не блокировалась.\n// – Если ошибка не равна http.ErrServerClosed (это «нормальный» код закрытия), логируем и завершаем приложение через log.Fatalf.\n\tgo func() {\n\t\tlog.Printf(\"🚀 Starting server on %s\", srv.Addr)\n\t\tif err :=  srv.ListenAndServe(); err != nil \u0026\u0026 err != http.ErrServerClosed {\n\t\t\tlog.Fatalf(\"failed to start server: %v\", err)\n\t\t}\n\t}()\n\n// 5. Ловим системные сигналы для graceful-shutdown\n\t// Настраиваем ловлю сигнала прерывания (Ctrl+C / kill)\n//   – Создаём канал quit для системных сигналов.\n// – signal.Notify подписывается на SIGINT (Ctrl+C) и SIGTERM (kill).\n// – \u003c-quit блокируется до получения одного из этих сигналов.\n\tquit := make(chan os.Signal, 1)\n\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n\t\u003c-quit\n\tlog.Println(\"🔌 Shutdown signal received, exiting...\")\n\n// 6. Останавливаем сервер с таймаутом (пока не обрывать запросы)\n\t// Даем серверу 5 секунд на «тихую» остановку\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tif err := srv.Shutdown(ctx); err != nil {\n\t\tlog.Fatalf(\"server forced to shutdown: %v\", err)\n\t}\n//   – Создаём контекст с таймаутом (5 секунд), чтобы не ждать бесконечно.\n// – srv.Shutdown(ctx) сообщит серверу:\n// • перестать принимать новые соединения;\n// • дать «живущим» запросам завершиться в течение времени таймаута;\n// • после чего принудительно закрыть остатки.\n// – Если в пределах 5 секунд все запросы не завершились — Shutdown вернёт ошибку, и мы логируем фатал.\n// – Если всё прошло успешно — пишем в лог об удачном завершении.\n\n\tlog.Println(\"🛑 Server stopped gracefully\")\n}\n```\n\nПояснения шагов:\n- gin.Default() — заводит стандартный HTTP-сервер с логированием и recover-middleware.\n- Роуты регистрируются привычным router.GET/....\n- Создаётся стандартный http.Server, в поле Handler передаётся наш Gin-router.\nsrv.ListenAndServe() запускается в отдельной горутине, чтобы основной поток мог «сидеть» и ждать сигнала остановки.\n- Через os.Signal и signal.Notify ловим Ctrl+C (SIGINT) или kill (SIGTERM).\n- srv.Shutdown(ctx) — это встроенный метод Go-сервера, который: • перестаёт принимать новые подключения;\n• даёт текущим обработчикам (Gin-хендлерам) до 5 секунд на завершение;\n• затем принудительно закрывает оставшиеся.\nТак мы получаем «мягкую» остановку сервера, не «режем» на лету открытые HTTP-соединения.\n\n\nЗачем так делают?\n• Грейсфул-шадоун (graceful shutdown) нужен для того, чтобы при рестарте/обновлении/остановке сервера не обрывать «на лету» активные HTTP-сессии или транзакции.\n• Это критично в боевых системах, чтобы клиенты получали ответы, а не «обрыв соединения».\n• Использование http.Server + контекст + сигналов OS — стандартный паттерн в Go для управления жизненным циклом HTTP-сервисов.\n\nЧто здесь улучшено:\n- Убираем избыточный os.Exit(1) после log.Fatal (он и так вызывает os.Exit(1)).\n- Пишем понятный log.Fatalf(\"…: %v\", err) вместо errors.Error(err).\n- Используем стандартный http.Server для возможности graceful-shutdown.\n- Ловим сигналы SIGINT/SIGTERM и даём серверу 5 секунд на корректное завершение активных соединений.\n\n\n# Шаг 5: Обновление тестов До нормального уровня (табличные тесты)\nОбновите тесты:\n\n```go\n// performRequest создаёт контекст Gin и вызывает handler.\n// Возвращает recorder с накопленным ответом.\nfunc performRequest(\n\thandler gin.HandlerFunc,\n\tmethod, path string,\n\tbody io.Reader,\n) *httptest.ResponseRecorder {\n\t// ResponseRecorder будет записывать статус, заголовки и тело\n\trecorder := httptest.NewRecorder()\n\n\t// Получаем пустой Router и Context\n\tctx, _ := gin.CreateTestContext(recorder)\n\n\t// Формируем HTTP-запрос и помещаем его в ctx\n\treq := httptest.NewRequest(method, path, body)\n\tctx.Request = req\n\n\t// Вызываем handler\n\thandler(ctx)\n\n\treturn recorder\n}\n```\n\nДавай разберём эту функцию — performRequest — шаг за шагом, очень просто, как для ребёнка. Представь, что у нас есть крохотный ресторан (наш HTTP-сервер), и мы хотим научиться проверять, что повар (наш хендлер) правильно готовит блюдо (отдаёт правильный ответ). Но чтобы не устраивать полный запуск ресторана, мы делаем «мини-кухню» в тесте. Вот как это выглядит:\n\n### Зачем нужна функция performRequest?\n– Мы не хотим каждый раз поднимать настоящий HTTP-сервер, открывать порты и отправлять настоящие запросы.\n– Вместо этого мы создаём «двоичный» тест, где загружаем только нужного нам повара (хендлер) и передаём ему искусственный запрос.\n– Функция performRequest автоматизирует этот процесс: она готовит среду, даёт хендлеру «запрос» и возвращает нам «ответ», чтобы мы могли его проверить.\n\n###Какие у неё входные и выходные данные?\n\nВход:\n- handler gin.HandlerFunc — это как повар: кусочек кода, который умеет принимать запрос и готовить ответ.\n- method и path (строки) — тип запроса (GET, POST и т. д.) и адрес (например, /hello).\n- body io.Reader — тело запроса (например, JSON или форма), но в нашем случае мы часто ставим nil, потому что тело нам не нужно.\n\nВыход:\n- *httptest.ResponseRecorder — это блокнот и камера, которые записывают всё, что повар отправляет в ответ: какой статус (200, 404…), какие заголовки (Content-Type) и какое тело (JSON-строка).\n\n\n### Что происходит внутри функции, шаг за шагом: \n#### Шаг 3.1. Создаём «блокнот» для ответа\n```go\nrecorder := httptest.NewRecorder()\n```\nПредставь, что это пустая тетрадка, куда повар будет записывать, что он приготовил: статус, заголовки и тело. \n\n\n#### Шаг 3.2. Готовим маленькую «кухню» Gin\n```go\nctx, _ := gin.CreateTestContext(recorder)\n```\n\n- gin.CreateTestContext даёт нам два животика: пустой маршрутизатор (мы его не используем) и контекст (ctx), в котором хранится и информация о запросе, и «блокнот» для ответа.\n- Мы передаём recorder туда, чтобы Gin знал: «Записывай ответ именно в эту тетрадку». \n\n\n#### Шаг 3.3. Формируем искусственный HTTP-запрос\n```go\nreq := httptest.NewRequest(method, path, body)\nctx.Request = req\n```\n\n- httptest.NewRequest создаёт объект запроса так, будто его прислал клиент (браузер или другая программа).\n- method и path определяют, какой запрос — например, GET на /hello.\n- body — если нужно, мы можем передать JSON или форму.\n- Затем мы говорим нашему контексту ctx, что у него теперь есть запрос: ctx.Request = req. \n\n#### Шаг 3.4. Вызываем хендлер (повара)\n```go\nhandler(ctx)\n```\n\n- Мы передаём контекст с нашим искусственным запросом прямо в функцию-повара.\n- Повар прочитает ctx.Request, обработает запрос (прочитает путь, параметры, тело) и напишет результат в ctx.Writer. А ctx.Writer как раз связан с нашим recorder. \n\n#### Шаг 3.5. Возвращаем записанный ответ\n```go\nreturn recorder\n```\n\n- В recorder уже записано всё, что хендлер хотел отправить клиенту: например, код 200, заголовок Content-Type: application/json; charset=utf-8 и тело {\"text\":\"Hello, World!\"}.\n- Мы возвращаем этот recorder из функции, чтобы в тесте проверить:\n\n• Правильный ли статус?\n\n• Правильный ли заголовок Content-Type?\n\n• Правильный ли JSON-ответ?\n\n\n### Зачем это нужно и где встречается?\n- В юнит-тестах HTTP-сервисов на Go с фреймворком Gin (и не только).\n- Позволяет очень быстро и локально проверить логику одного конкретного обработчика, без кучи побочных эффектов (нет реальной сети, нет реальных файлов, нет базы данных — только чистый код).\n- Если у тебя много мелких хендлеров (регистрация, логин, получение пользователя и т. п.), каждый можно протестировать таким способом.\n\nИтог:\nФункция performRequest — это мини-фабрика по созданию искусственного HTTP-запроса и захвату ответа, чтобы мы могли в тестах спокойно и надёжно проверять, как наш хендлер реагирует на разные запросы.\n\n\n## пример самого теста для хендлера\n```go\nfunc TestHelloHandler(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\n\ttests := []struct {\n\t\tname                string\n\t\tmethod              string\n\t\tpath                string\n\t\texpectedStatusCode  int\n\t\texpectedContentType string\n\t\texpectedBody        Message\n\t}{\n\t\t{\n\t\t\tname:                \"GET hello returns 200 JSON\",\n\t\t\tmethod:              http.MethodGet,\n\t\t\tpath:                \"/hello\",\n\t\t\texpectedStatusCode:  http.StatusOK,\n\t\t\texpectedContentType: \"application/json; charset=utf-8\",\n\t\t\texpectedBody:        Message{Text: \"Hello, World!\"},\n\t\t},\n\t\t// при необходимости можно добавить больше кейсов\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc // для параллельного запуска\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// выполняем запрос\n\t\t\trecorder := performRequest(HelloHandler, tc.method, tc.path, nil)\n\n\t\t\t// проверяем статус-код\n\t\t\trequire.Equal(t, tc.expectedStatusCode, recorder.Code)\n\n\t\t\t// проверяем заголовок Content-Type\n\t\t\trequire.Equal(t, tc.expectedContentType, recorder.Header().Get(\"Content-Type\"))\n\n\t\t\t// разбираем JSON-ответ\n\t\t\tvar msg Message\n\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), \u0026msg)\n\t\t\trequire.NoError(t, err, \"response must be valid JSON\")\n\n\t\t\t// проверяем тело ответа\n\t\t\trequire.Equal(t, tc.expectedBody, msg)\n\t\t})\n\t}\n}\n\nfunc TestGoodbyeHandler(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\n\ttests := []struct {\n\t\tname                string\n\t\tmethod              string\n\t\tpath                string\n\t\texpectedStatusCode  int\n\t\texpectedContentType string\n\t\texpectedBody        Message\n\t}{\n\t\t{\n\t\t\tname:                \"GET goodbye returns 200 JSON\",\n\t\t\tmethod:              http.MethodGet,\n\t\t\tpath:                \"/goodbye\",\n\t\t\texpectedStatusCode:  http.StatusOK,\n\t\t\texpectedContentType: \"application/json; charset=utf-8\",\n\t\t\texpectedBody:        Message{Text: \"Goodbye, World!\"},\n\t\t},\n\t\t// при необходимости можно добавить больше кейсов\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc // для параллельного запуска\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// выполняем запрос\n\t\t\trecorder := performRequest(GoodbyeHandler, tc.method, tc.path, nil)\n\n\t\t\t// проверяем статус-код\n\t\t\trequire.Equal(t, tc.expectedStatusCode, recorder.Code)\n\n\t\t\t// проверяем заголовок Content-Type\n\t\t\trequire.Equal(t, tc.expectedContentType, recorder.Header().Get(\"Content-Type\"))\n\n\t\t\t// разбираем JSON-ответ\n\t\t\tvar msg Message\n\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), \u0026msg)\n\t\t\trequire.NoError(t, err, \"response must be valid JSON\")\n\n\t\t\t// проверяем тело ответа\n\t\t\trequire.Equal(t, tc.expectedBody, msg)\n\t\t})\n\t}\n}\n```\n\nДавай разберём этот тестовый код совершенно по-простому, шаг за шагом, как будто рассказываем новичку или даже ребёнку. Представь, что у нас есть функция (хендлер) HelloHandler, которая на запрос “/hello” отвечает строкой “Hello, World!” в формате JSON. Мы хотим написать автоматический тест, чтобы убедиться, что она всегда так работает.\n\n### Зачем вообще нужны тесты?\n- Чтобы без твоего вмешательства проверить, что код делает то, что должно.\n- Если что-то случайно поломается, тест сразу об этом сообщит.\n\n## что такое smoke-тест и в чем разница от unit-теста\n### 🔥 Smoke-тест (тест \"дымовой проверки\")\nЭто базовая проверка, чтобы убедиться, что система вообще запускается и не падает сразу. Название пошло от \"проверки на дым\": включили устройство — не задымилось, значит, можно тестировать дальше.\n\n- Цель: Быстро удостовериться, что основная функциональность работает.\n- Уровень: Обычно проводится после сборки, перед глубоким тестированием.\n\nПримеры:\n- Страница загружается и отвечает 200 OK.\n- API отдаёт хотя бы базовый ответ.\n- Базовые зависимости (БД, Redis, конфиги) доступны.\n\n### 🧪 Unit-тест (модульный тест)\nЭто подробная проверка маленьких, изолированных частей кода — например, функций, методов или компонентов.\n\n- Цель: Проверить, что конкретный блок логики работает как надо.\n- Уровень: Низкий — не зависит от внешней среды.\n\nПримеры:\n- Тестирование функции расчёта налога.\n- Проверка обработки edge-case данных.\n- Убедиться, что метод возвращает нужный результат.\n\n\n### Какие еще виды тестов существуют?\nуществует множество видов тестирования, и каждый из них играет свою роль в обеспечении качества ПО. Вот краткий обзор самых распространённых:\n\n🧪 Unit-тесты\nТестируют отдельные функции или методы.\n\nИзолированы от внешней среды.\n\nБыстрые и точечные.\n\n🔗 Integration-тесты\nПроверяют взаимодействие между модулями.\n\nНапример, соединение между API и базой данных.\n\nУловят баги, возникающие при склейке компонентов.\n\n🎭 End-to-End (E2E) тесты\nИмитируют реальное поведение пользователя.\n\nПолный путь: от ввода до ответа системы.\n\nМедленные, но эффективные для проверки общего флоу.\n\n💨 Smoke-тесты\nПоверхностная проверка, что система вообще работает.\n\nИспользуются после сборки — чтобы убедиться, что ничего не сломалось.\n\n🧃 Sanity-тесты\nБыстрая проверка, что мелкие багфиксы или изменения не нарушили основную функциональность.\n\nПохожи на smoke, но более сфокусированы.\n\n🛠 Regression-тесты\nПроверка, что новые изменения не сломали существующий функционал.\n\nЧасто автоматизируются и запускаются в CI/CD.\n\n👤 Acceptance-тесты\nОценивают, соответствует ли система требованиям заказчика.\n\nЧасто пишутся совместно с бизнесом.\n\n🧼 UI/UX тесты\nПроверяют интерфейс и пользовательский опыт.\n\nМожет включать визуальную регрессию, удобство взаимодействия и читаемость.\n\n📊 Performance и Load-тесты\nОценивают скорость и устойчивость системы под нагрузкой.\n\nНапример, выдерживает ли сайт 1000 запросов в секунду.\n\n#### Сигнатура unit-теста\n```go\nfunc TestHelloHandler(t *testing.T) { … }\n```\n- Любой тест в Go начинается с Test и принимает t *testing.T.\n- Инструмент go test найдёт все функции, начинающиеся с Test, и выполнит их.\n\n#### Отключаем лишние логи\n```go\ngin.SetMode(gin.TestMode)\n```\n- Gin по умолчанию может много писать в консоль (логи запросов).\n- В режиме TestMode он тихонько работает, не мешает выводом.\n\n\n#### Описываем входные данные и ожидаемые результаты (table-driven тест)\n```go\ntests := []struct {\n  name                string\n  method              string\n  path                string\n  expectedStatusCode  int\n  expectedContentType string\n  expectedBody        Message\n}{\n  {\n    name:                \"GET hello returns 200 JSON\",\n    method:              http.MethodGet,\n    path:                \"/hello\",\n    expectedStatusCode:  http.StatusOK,\n    expectedContentType: \"application/json; charset=utf-8\",\n    expectedBody:        Message{Text: \"Hello, World!\"},\n  },\n}\n```\n- tests — это список кейсов (сценариев), которые мы хотим проверить.\n- Для каждого кейса указываем:\n\n• name — человекочитаемое название, чтобы понять, что именно проверяется.\n\n• method и path — что мы отправляем хендлеру (GET на /hello).\n\n• expectedStatusCode — какой HTTP-код мы хотим получить (200 OK).\n\n• expectedContentType — какой заголовок “Content-Type” мы хотим увидеть.\n\n• expectedBody — какую структурированную информацию (JSON) мы ждём в теле ответа.\n\n\n#### Перебираем все кейсы\n```go\nfor _, tc := range tests {\n  tc := tc              // фикс для параллели\n  t.Run(tc.name, func(t *testing.T) {\n    t.Parallel()        // запускаем тесты параллельно\n    …\n  })\n}\n```\n- for _, tc := range tests значит “для каждого теста из списка”.\n- t.Run(tc.name, func(t *testing.T) { … }) создаёт отдельный подпроцесс (subtest) с именем tc.name.\n- t.Parallel() позволяет этим подпроцессам выполняться одновременно, ускоряя общий прогон тестов.\n\n\n#### Внутри каждого подпроцесса делаем три вещи: \n##### 6.1. Выполняем наш хендлер с искусственным запросом\n```go\nrecorder := performRequest(HelloHandler, tc.method, tc.path, nil)   \n```   \n- performRequest — это вспомогательная функция, которая:\n\n• создаёт фиктивный HTTP-запрос (метод + путь),\n\n• передаёт его в хендлер,\n\n• возвращает recorder — объект, в котором записаны статус, заголовки и тело ответа.\n\n##### 6.2. Проверяем статус-код\n```go\nrequire.Equal(t, tc.expectedStatusCode, recorder.Code)      \n```\n\n- recorder.Code — это код ответа (например, 200).\n- require.Equal (из библиотеки testify) сравнивает два числа и сразу останавливает тест и выводит ошибку, если они не совпадают. \n\n##### 6.3. Проверяем заголовок Content-Type\n```go\nrequire.Equal(t, tc.expectedContentType, recorder.Header().Get(\"Content-Type\")) \n```     \n- recorder.Header().Get(\"Content-Type\") читает, какой “Content-Type” хендлер установил.\n- Сравниваем с ожидаемым (обычно “application/json; charset=utf-8”). \n\n##### 6.4. Проверяем тело ответа (JSON)\n```go      \n  var msg Message      \n  err := json.Unmarshal(recorder.Body.Bytes(), \u0026msg)      \n  require.NoError(t, err, \"response must be valid JSON\")      \n  require.Equal(t, tc.expectedBody, msg)   \n```   \n- recorder.Body.Bytes() возвращает «сырые» байты ответа, например {\"text\":\"Hello, World!\"}.\n- json.Unmarshal пытается распарсить эти байты в переменную msg типа Message (у нас это простая структура с одним полем Text).\n- require.NoError проверяет, что распарсилось без ошибок.\n- И наконец сравниваем полученную структуру msg с тем, что мы ожидали (tc.expectedBody).\n\n\n#### Результат\n- Если всё совпало (код, заголовок, JSON-тело), тест считается пройденным.\n- Если хоть что-то не совпало, require сразу выведет ошибку с подробностями, какой именно чек провалился.\n\n#### Где такое встречается?\n- В любом Go-проекте, где есть HTTP-серверы и хочется проверять API-эндоинты без поднятия реального сервера и без реальных сетевых запросов.\n- Особенно часто это используется с фреймворком Gin (или похожими), но принцип в целом одинаковый: эмулируем контекст, передаём запрос в хендлер и смотрим, что он “напишет” в ответ.\n\nТаким образом, этот тест — это автоматическая проверка, что HelloHandler всегда отдаёт ровно то, что мы от него ожидаем, в любых условиях.\n\n\n### Что делать со swagger роутом\nПокрывать тестами маршрут Swagger — не обязательно и, как правило, нецелесообразно. Вот почему:\n\n🔍 Назначение маршрута\nЭтот маршрут служит только для отображения UI документации — он не реализует бизнес-логику и не влияет на поведение API.\n\nПлатформа Swagger и её обработчик WrapHandler уже протестированы в рамках библиотеки, которую вы используете.\n\n✅ Когда стоит протестировать\nЕсли вы кастомизировали поведение Swagger UI — например, изменили доступность, добавили авторизацию, внедрили динамическую генерацию документации — тогда тесты могут быть полезны.\n\nВ случае CI/CD можно проверить, что маршрут существует и отдаёт 200 OK, чтобы убедиться, что документация доступна на деплое.\n\n🧪 Пример простого smoke-теста\nЕсли всё же хочется покрыть базовой проверкой:\n\n```go\nfunc TestSwaggerRouteAvailable(t *testing.T) {\n    gin.SetMode(gin.TestMode)\n\n    router := gin.New()\n    router.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\n    ts := httptest.NewServer(router)\n    defer ts.Close()\n\n    resp, err := http.Get(ts.URL + \"/swagger/index.html\")\n    require.NoError(t, err)\n    require.Equal(t, http.StatusOK, resp.StatusCode)\n}\n```\n\n🔍 Что делает этот тест\nПроверяет, что маршрут Swagger действительно доступен.\n\nУбеждается, что сервер не отдает ошибку (например, 404 или 500).\n\nНе валидирует содержимое, структуру HTML, версию Swagger — только доступность.\n\n🔥 Почему это smoke-тест\nОн поверхностный и быстрый — проверка \"жива ли документация\".\n\nНе углубляется в бизнес-логику или технические детали.\n\nИдеально подходит для проверки после сборки, перед полноценным запуском тестов.\n\nЕсли бы мы начали валидировать HTML, параметры Swagger конфигурации, или логику кастомного рендеринга — это был бы уже integration-тест или даже UI-тест.\n\nНо в большинстве случаев это — не приоритет. Лучше сфокусироваться на тестах бизнес-логики API, контроллеров, middleware и прочего функционала.\n\n#### Смок по шагам\n😊 Представь, что мы хотим проверить: работает ли страничка Swagger, где показывается документация по нашему API. Мы пишем тест, похожий на маленький автоматический инспектор, который откроет эту страницу и убедится, что она не сломалась.\n\nВот как это работает по шагам — максимально просто:\n\n🧩 Шаг 1: Подготовим игровую площадку\n```go\nrouter := gin.New()\nrouter.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n```\n➡️ Мы создаём маршруты, как будто запускаем мини-сервер Gin прямо внутри теста.\n\n🧩 Шаг 2: Включаем мини-сервер\n```go\nts := httptest.NewServer(router)\ndefer ts.Close()\n```\n\n➡️ Тут мы запускаем тестовый сервер, он настоящий, но временный и работает только для теста. После теста сам выключится.\n\n🧩 Шаг 3: Отправляем запрос как будто из браузера\n```go\nresp, err := http.Get(ts.URL + \"/swagger/index.html\")\n```\n\n➡️ Это как открыть в браузере адрес: http://тестовый-сервер/swagger/index.html и посмотреть, что вернётся.\n\n🧩 Шаг 4: Проверяем, не сломалось ли\n```go\nrequire.NoError(t, err)\nrequire.Equal(t, http.StatusOK, resp.StatusCode)\n```\n➡️ Мы говорим:\n\n- 💬 “Ошибок не должно быть”\n- 🟢 “Страница должна ответить кодом 200 OK, то есть ‘всё хорошо!’”\n\n🧩 Шаг 5: Включаем \"режим тишины\"\n```go\ngin.SetMode(gin.TestMode)\n```\n➡️ Это как нажать кнопку “Тест” — выключаем лишние сообщения в консоль, чтобы они не мешали тесту.\n\n#### 📦 Итого:\n- Это smoke-тест — очень простой, быстрый и важный. Он говорит:\n- “Swagger жив? Да! Можно двигаться дальше и тестировать остальную систему.”\n\n\n\n## Шаг 6: Обновление GitHub Actions\n\nЧтобы swag работал в GitHub Actions, его нужно установить вручную в рамках workflow. Вот как это делается:\n📌 Что происходит:\n\n- go install — скачивает и ставит swag CLI.\n- $GITHUB_PATH — добавляет путь к исполняемым файлам Go (~/go/bin) в PATH, чтобы следующая команда могла использовать swag.\n\nОбновите файл .github/workflows/ci.yml, чтобы включить генерацию документации Swagger:\n```yaml\n\nname: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: '1.23'\n\n    - name: Install Swagger\n      run: |\n          go install github.com/swaggo/swag/cmd/swag@v1.8.12\n          echo \"$(go env GOPATH)/bin\" \u003e\u003e $GITHUB_PATH\n\n    - name: Swag version\n      run: swag --version\n\n    - name: Generate Swagger docs\n      run: swag init\n\n    - name: Build\n      run: go build -v ./...\n\n    - name: Test\n      run: go test -v ./...\n```\n## Заключение\nТеперь у вас есть простой REST API на Go с парой хендлеров, минимальным набором тестов (юнит табличные и смок), интеграцией Swagger для документации и настроенным GitHub Actions для CI/CD. Вы можете расширять этот пример, добавляя больше функциональности, тестов и улучшений по мере необходимости.\n\n\n# Часть 3\nдавай теперь добавим docker к этому проекту\n\n🐳 Docker — это как чемодан с заранее собранной средой, в которой твой Go-приложение живёт и работает, независимо от того, где его запустят. Вот простое объяснение:\n\n#### 🧩 Что такое Docker?\nЭто инструмент, позволяющий упаковать приложение и всё, что ему нужно — зависимости, конфиги, версии — в один контейнер.\n\nКонтейнер — как мини-компьютер с твоим приложением внутри. Его можно запускать везде одинаково: на сервере, локально, в облаке.\n\n#### основные термины и понятия Docker\nDocker: Контейнеризация приложений\nТермин\tОбъяснение\nDocker\tПлатформа для упаковки приложения и его окружения в контейнер.\nКонтейнер\tИзолированная среда, где работает приложение. Лёгкий, быстрый, переносимый.\nОбраз (Image)\tШаблон для создания контейнера. Содержит всё необходимое: код, зависимости, конфиги.\nDockerfile\tСкрипт, описывающий, как собрать образ.\nVolume\tХранилище данных, которое можно подключить к контейнеру.\nNetwork\tВиртуальная сеть для связи контейнеров между собой.\nRegistry\tХранилище образов. Например, Docker Hub или GitLab Container Registry.\nTag\tМетка версии образа. Например, myapp:1.0.3.\nCompose\tИнструмент для запуска нескольких контейнеров вместе (docker-compose.yml).\nMulti-stage build\tСборка образа в несколько этапов, чтобы финальный образ был лёгким и безопасным.\n\n#### ⚙️ Зачем он нужен Go-разработчику?\n✅ 1. Гарантированное окружение\nGo хорошо компилируется в бинарники, но окружение (переменные, версии библиотек, хранилища) всё равно важно.\n\nDocker гарантирует, что у всех участников команды и у прод-серверов будет одинаковая среда.\n\n🚀 2. Простой деплой\nС Docker можно задеплоить приложение буквально одной командой: docker run.\n\nНе нужно вручную настраивать сервер, устанавливать Go, пакеты, зависимости.\n\n🔁 3. Лёгкое масштабирование\nКаждый Go-сервис можно упаковать в отдельный контейнер.\n\nУправлять контейнерами удобно через Docker Compose, Kubernetes и CI/CD пайплайны.\n\n🧪 4. Автоматизация тестов и сборки\nМожно настроить pipeline, который в CI билдит, тестит и выкатывает образ.\n\nЭто упрощает поддержку и ускоряет выпуск новых версий.\n\n📦 В чём кайф для Go?\nGo — это язык, заточенный под микросервисы и высокую скорость сборки\n\n\n\n#### окей, расскажи зачем добавлять docker к микросервису на go\n🚢 Отличный вопрос — Docker и микросервисы — как хлеб и масло, особенно в Go-проектах. Объясню, зачем вообще «докеризовать» микросервис:\n\n🧰 1. Изоляция окружения\nDocker упаковывает всё: бинарник Go, зависимости, конфиги — в контейнер, работающий одинаково на любой машине.\n\nЗабудь про «на моём компе работает, а на сервере нет» — в контейнере будет всегда то же самое окружение.\n\n🚀 2. Лёгкий деплой\nКонтейнер можно запустить в любой среде: локально, на сервере, в Kubernetes, в облаке.\n\nНикаких ручных установок Go или зависимостей — только docker run.\n\n🔄 3. Масштабируемость\nОдин микросервис = один контейнер. Нужно больше инстансов? Просто запускаем больше копий.\n\nЭто удобно, особенно при использовании оркестраторов: Docker Compose, Kubernetes, Nomad.\n\n🧪 4. Единый pipeline для CI/CD\nВ CI легко прогнать тесты, собрать образ, опубликовать его, и задеплоить — всё через один Dockerfile.\n\nРаботает хорошо с GitHub Actions, GitLab CI и другими инструментами.\n\n🛡 5. Безопасность и стабильность\nКонтейнер — как песочница. Он изолирован от хост-системы, что снижает риск случайных конфликтов или уязвимостей.\n\nМожно задать точную версию Go, Linux, нужных пакетов.\n\n🔮 Бонус: проще работать в команде\nУ каждого разработчика одинаковая среда, независимо от ОС.\n\nДаже новичок может просто запустить docker compose up и работать.\n\nХочешь — могу накидать Dockerfile под типичный микросервис на Gin с Swagger и ENV переменными. Или показать, как собрать multi-stage образ, чтобы он был компактным и безопасным. Какой вариант тебе интереснее? 🛠️\n\nДавайте добавим Docker в наш проект. Мы создадим Dockerfile для контейнеризации нашего приложения и обновим GitHub Actions для сборки и тестирования в Docker контейнере.\n\n## Шаг 1: Создание Dockerfile\nСоздайте файл Dockerfile в корневой директории проекта и добавьте следующий код:\n```sh\n# Используем официальный образ Go как базовый образ\nFROM golang:1.23-alpine\n\n# Устанавливаем рабочую директорию\nWORKDIR /app\n\n# Копируем go.mod и go.sum файлы\nCOPY go.mod go.sum ./\n\n# Устанавливаем зависимости\nRUN go mod download\n\n# Копируем остальные файлы приложения\nCOPY . .\n\n# Собираем приложение\nRUN go build -o main .\n\n# Указываем, что контейнер будет запускать собранное приложение\nCMD [\"./main\"]\n```\n\n\nсобираем образ:\n```sh\ndocker build -t gh-actions-sbs .\n```\n\nзапускаем контейнер по имени:\n```sh\ndocker run gh-actions-sbs:latest\n```\n\nЧтобы запустить Docker-контейнер на конкретном порте, нужно использовать флаг -p в команде docker run. Он позволяет сопоставить порт хоста с портом внутри контейнера.\n\n🔧 Синтаксис команды\n```sh\ndocker run -p \u003cпорт_хоста\u003e:\u003cпорт_контейнера\u003e \u003cимя_образа\u003e\n```\nНапример, если твое приложение внутри контейнера слушает порт 8080, а ты хочешь, чтобы оно было доступно на порту 3000 на твоем компьютере:\n\n```sh\ndocker run -p 3000:5000 gh-actions-sbs:latest\n```\n\n3000 — порт на твоем компьютере (хосте)\n\n8080 — порт внутри контейнера, на котором работает приложение\n\nЗапуск тестов в контейнере:\n```sh\ndocker run --rm gh-actions-sbs go test -v ./...\n```\n\n#### 😶 Если браузер «молчит» и не видит сайт из Docker-контейнера \n— значит, где-то нарушена цепочка: приложение → контейнер → порт → хост → браузер. Давай разберёмся по шагам:\n\n🔍 1. Приложение слушает правильный адрес?\nВ Go-приложении должно быть:\n\ngo\nhttp.ListenAndServe(\"0.0.0.0:8080\", nil)\n❗️Если стоит \"127.0.0.1\" — контейнер не отдаёт наружу, и браузер не сможет достучаться.\n\n📦 2. Порт проброшен при запуске?\nТы должен запускать контейнер с флагом -p, например:\n\nbash\ndocker run -p 8080:8080 myapp\nПервый 8080 — порт на твоём компьютере\n\nВторой 8080 — порт внутри контейнера\n\n🧱 3. Проверка: контейнер работает?\nВыполни:\n\nbash\ndocker ps\nУбедись, что контейнер запущен и порт проброшен.\n\n🌐 4. Открытие в браузере\nОткрой:\n\nhttp://localhost:8080\nЕсли ты в WSL — попробуй:\n\nhttp://127.0.0.1:8080\nИли IP адрес WSL:\n\nbash\nip addr | grep inet\n🧪 5. Быстрая диагностика\nВ терминале WSL:\n\nbash\ncurl localhost:8080\nЕсли получаешь ответ — значит, приложение работает, и проблема в доступе из браузера.\n\n🧠 Частые причины:\nПриложение слушает 127.0.0.1, а не 0.0.0.0\n\nПорт не проброшен (-p)\n\nФаервол или антивирус блокирует порт\n\nПриложение упало внутри контейнера — проверь логи:\n\nbash\ndocker logs \u003ccontainer_id\u003e\n\n## Шаг 2: Обновление GitHub Actions\nОбновите файл .github/workflows/ci.yml, чтобы включить сборку и тестирование в Docker контейнере:\n```yaml\n\nname: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: '1.23'\n\n    - name: Install Swag\n      run: go install github.com/swaggo/swag/cmd/swag@v1.8.12\n          echo \"$(go env GOPATH)/bin\" \u003e\u003e $GITHUB_PATH\n\n    - name: Swag version\n      run: swag --version\n\n    - name: Generate Swagger docs\n      run: swag init\n\n    - name: Build Docker image\n      run: docker build -t go-rest-api .\n\n    - name: Run tests in Docker container\n      run: docker run --rm go-rest-api go test -v ./...\n\n    - name: Run application in Docker container\n      run: timeout 15s docker run --rm -p 8080:8080 go-rest-api\n```\n\nШаг 3: Запуск и проверка локально\nСоберите Docker образ локально:\n```sh\n\ndocker build -t go-rest-api .\n```\n\nЗапустите контейнер:\n```sh\ndocker run --rm -p 8080:8080 go-rest-api\n```\n\nОткройте браузер и перейдите по адресам http://localhost:8080/hello и http://localhost:8080/goodbye, чтобы убедиться, что API работает.\n\nОткройте браузер и перейдите по адресу http://localhost:8080/swagger/index.html, чтобы увидеть документацию Swagger.\n\n\n## graceful shutdown при падении healthcheck.\n\nПо-сеньёрному — значит не просто «запустилось», а гарантированно работает, и если нет — джоба падает. Вот несколько подходов, которые используют опытные разработчики Go-проектов в GitHub Actions:\n\n🧰 4. Используй готовый GitHub Action\nНапример: zethuman/healthcheck\n\nyaml\n- name: Container healthcheck\n  uses: zethuman/healthcheck@v0.0.1\n  with:\n    name: my-container\n    timeout: 180\n    interval: 2\nПроверяет статус контейнера и завершает джобу, если он unhealthy\n\n🧼 5. В Go-приложении — отдельный /health эндпоинт\ngo\nhttp.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n  w.WriteHeader(http.StatusOK)\n})\nЭто стандарт для Kubernetes, Docker и CI/CD — не игнорируй\n\n\n Если healthcheck падает — это сигнал, что приложение не в порядке, и его стоит корректно завершить. Вот как можно реализовать graceful shutdown в Gin при сбое healthcheck:\n\n🧠 Общая идея\nПериодически проверяем состояние (например, SQL, Redis, ENV).\n\nЕсли проверка не проходит — инициируем shutdown.\n\nЗавершаем сервер через http.Server.Shutdown() с таймаутом.\n\nПример:\n```go\nfunc checkDependencies() bool {\n\t// проверкИ коннектов к бд редиске и тп\n\n\treturn true\n}\n```\n\n#### main c health check и graceful:\n```go\nfunc main() {\n\t// 1. Инициализируем Gin с дефолтными middleware (Logger, Recovery)\n\trouter := gin.Default()\n\n\t// 2. Регистрируем любые эндпоинты\n\t// Healthcheck endpoint\n\trouter.GET(\"/health\", func(c *gin.Context) {\n\t\tif !checkDependencies() {\n\t\t\tc.JSON(http.StatusServiceUnavailable, gin.H{\"status\": \"fail\"})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(http.StatusOK, gin.H{\"status\": \"ok\"})\n\t})\n\trouter.GET(\"/hello\", HelloHandler)\n\trouter.GET(\"/goodbye\", GoodbyeHandler)\n\n\t// Роут для Swagger UI\n\trouter.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\n\t// 3. Оборачиваем router в http.Server\n\tsrv := \u0026http.Server{\n\t\tAddr:    \"0.0.0.0:8080\",\n\t\tHandler: router,\n\t}\n\n\t// 4. Запускаем сервер в отдельной горутине\n\tgo func() {\n\t\tlog.Printf(\"🚀 Starting server on %s\", srv.Addr)\n\t\tif err := srv.ListenAndServe(); err != nil \u0026\u0026 err != http.ErrServerClosed {\n\t\t\tlog.Fatalf(\"failed to start server: %v\", err)\n\t\t}\n\t}()\n\n\t// Канал для внутреннего shutdown при падении healthcheck\n\tinternalShutdown := make(chan struct{})\n\n\t// Мониторинг состояния\n\tgo func() {\n\t\tfor {\n\t\t\ttime.Sleep(10 * time.Second)\n\t\t\tif !checkDependencies() {\n\t\t\t\tlog.Println(\"Healthcheck failed — initiating shutdown\")\n\t\t\t\tinternalShutdown \u003c- struct{}{}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 5. Ловим системные сигналы для graceful-shutdown\n\t// Настраиваем ловлю сигнала прерывания (Ctrl+C / kill)\n\tquit := make(chan os.Signal, 1)\n\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n\tselect {\n\tcase \u003c-quit:\n\t\tlog.Println(\"Получен сигнал завершения\")\n\tcase \u003c-internalShutdown:\n\t\tlog.Println(\"Healthcheck упал — graceful shutdown\")\n\t}\n\tlog.Println(\"🔌 Shutdown signal received, exiting...\")\n\n\t// 6. Останавливаем сервер с таймаутом (пока не обрывать запросы)\n\t// Даем серверу 5 секунд на «тихую» остановку\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\tif err := srv.Shutdown(ctx); err != nil {\n\t\tlog.Fatalf(\"server forced to shutdown: %v\", err)\n\t}\n\n\tlog.Println(\"Server stopped gracefully\")\n}\n```\n\n\n## встраиваем health check в CI\n```yaml\n    - name: Run App in Docker Container\n      run: docker run -d -p 8080:8080 gh-actions-sbs\n\n    - name: Health check (to 20s)\n      run: |\n        for i in {1..20}; do\n          if curl --fail http://localhost:8080/health; then\n            echo \"health - ok.\"\n            exit 0\n          fi\n          echo \"Loading...\"\n          sleep 1\n        done\n        echo \"health - bad.\"\n        exit 1\n```\n\n🔍 Как это работает\nCI запускает приложение в фоне (\u0026)\n\nЖдёт пару секунд (sleep 2)\n\nДелает curl на /health\n\nЕсли checkDependencies() вернёт false, /health отдаст 503, и curl завершится с ошибкой → CI упадёт\n\n### Добавь статус Ci в ридми\n\nзауди в Actions\nвыбери любой воркфлоу\nсоздай бейдж\nвсё\n[![CI](https://github.com/stevenstr/gh-actions-sbs/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/stevenstr/gh-actions-sbs/actions/workflows/ci.yaml)\n\n## Переезд на multy-stage building\n\n📦 Что такое multi-stage образ?\nЭто когда ты собираешь Docker-образ в несколько этапов. Каждый этап — это отдельная стадия, где ты что-то делаешь (сборка, копирование файлов, настройка). И в финальном образе ты берёшь только нужное, остальное — выбрасываешь.\n\n🏋️‍♂️ Зачем это нужно?\n🚫 Без multi-stage:\nОбраз включает всё подряд: компиляторы, лишние пакеты, кеши.\n\nОн тяжёлый и небезопасный — можно случайно оставить утечки или инструменты, которые не должны быть в проде.\n\n✅ С multi-stage:\nОбраз — минималистичный: только бинарник Go и нужные ENV.\n\nБезопаснее, легче, быстрее грузится и запускается.\n\n\n💡 Multi-stage образы — мощная техника, но у неё есть свои минусы. Давай разберёмся без фанатизма:\n\n⚠️ 1. Сложнее отладка\nКаждый этап сборки — отдельная среда. Не так просто посмотреть, что пошло не так, особенно если ошибка в промежуточной стадии.\n\nИногда нужно запускать образ с \"builder\"-стадии вручную, чтобы проверить сборку.\n\n🧠 2. Увеличение сложности Dockerfile\nСтановится больше инструкций, больше стадий, копирований и переменных.\n\nДля новичков — может выглядеть как \"тёмная магия\".\n\n🧪 3. Проблемы с кэшированием\nDocker кэширует слои, но при multi-stage один лишний COPY или RUN может сбросить кэш для всего этапа.\n\nЭто приводит к длинной сборке, особенно при большом проекте.\n\n🧱 4. Размер builder-образа не всегда игнорируется\nЕсли неправильно настроить, финальный образ может случайно включить лишние файлы.\n\nНужно аккуратно использовать COPY --from=builder, чтобы не затянуть весь /app.\n\n🧩 5. Ограничения в некоторых CI/CD системах\nНекоторые старые CI-инструменты не умеют хорошо работать с multi-stage.\n\nМожет быть сложно отлаживать образы на нестандартных раннерах или с кастомными pipeline.\n\n🎯 Но если всё грамотно настроить — плюсы перевешивают. Особенно для прод-сборки с компактным и безопасным образом.\n\n\n🧙‍♂️ Как это выглядит?\nВот «на пальцах» пример:\n\nDockerfile\n# Этап 1: сборка\nFROM golang:1.21 AS builder\nWORKDIR /app\nCOPY . .\nRUN go build -o myservice\n\n# Этап 2: финальный образ\nFROM alpine:latest\nWORKDIR /app\nCOPY --from=builder /app/myservice .\nCMD [\"./myservice\"]\n📉 Итог:\n🚀 Быстрее деплой\n\n📦 Меньше размер\n\n🔐 Меньше атак поверхностей\n\n\n## Заключение\nТеперь у вас есть простой REST API на Go с парой хендлеров, минимальным набором тестов, интеграцией Swagger для документации, Docker контейнеризацией и настроенным GitHub Actions для CI/CD. Вы можете расширять этот пример, добавляя больше функциональности, тестов и улучшений по мере необходимости.\n\n\n# Часть 4\nоформи теперь этот проект в классическом гошном стиле с папками cmd, internal и прочее\n\nДавайте реорганизуем проект в классическом Go стиле, используя папки cmd, internal, pkg и docs. Это поможет лучше структурировать код и сделать его более модульным и поддерживаемым.\n\ngo-rest-api/\n├── cmd/\n│   └── main.go\n├── internal/\n│   ├── api/\n│   │   └── handler.go\n│   ├── config/\n│   │   └── config.go\n│   └── swagger/\n│       └── docs.go\n├── pkg/\n│   └── utils/\n│       └── utils.go\n├── docs/\n│   └── swagger.json\n├── tests/\n│   └── main_test.go\n├── Dockerfile\n├── go.mod\n├── go.sum\n└── .github/\n    └── workflows/\n        └── ci.yml\n\n\nСодержимое файлов\ncmd/main.go\n\npackage main\n\nimport (\n    \"github.com/gin-gonic/gin\"\n    \"go-rest-api/internal/api\"\n    \"go-rest-api/internal/swagger\"\n)\n\nfunc main() {\n    router := gin.Default()\n\n    api.RegisterHandlers(router)\n    swagger.SwaggerInit(router)\n\n    router.Run(\":8080\")\n}\n\n\nСодержимое файлов\ninternal/api/handler.go\n\npackage api\n\nimport (\n    \"net/http\"\n    \"github.com/gin-gonic/gin\"\n)\n\ntype Message struct {\n    Text string `json:\"text\"`\n}\n\nfunc HelloHandler(c *gin.Context) {\n    message := Message{Text: \"Hello, World!\"}\n    c.JSON(http.StatusOK, message)\n}\n\nfunc GoodbyeHandler(c *gin.Context) {\n    message := Message{Text: \"Goodbye, World!\"}\n    c.JSON(http.StatusOK, message)\n}\n\nfunc RegisterHandlers(router *gin.Engine) {\n    router.GET(\"/hello\", HelloHandler)\n    router.GET(\"/goodbye\", GoodbyeHandler)\n}\n\nСодержимое файлов\ninternal/swagger/docs.go\npackage swagger\n\nimport (\n    \"github.com/swaggo/gin-swagger\"\n    \"github.com/swaggo/files\"\n    \"github.com/gin-gonic/gin\"\n)\n\n// @title Swagger Example API\n// @version 1.0\n// @description This is a sample server Petstore server.\n// @termsOfService http://swagger.io/terms/\n\n// @contact.name API Support\n// @contact.url http://www.swagger.io/support\n// @contact.email support@swagger.io\n\n// @license.name Apache 2.0\n// @license.url http://www.apache.org/licenses/LICENSE-2.0.html\n\n// @host localhost:8080\n// @BasePath /api/v1\n\n// @securityDefinitions.basic BasicAuth\n\n// @externalDocs.description OpenAPI\n// @externalDocs.url http://swagger.io\n\nfunc SwaggerInit(router *gin.Engine) {\n    router.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n}\n\n\n\nСодержимое файлов\ntests/main_test.go\n\n\nСодержимое файлов\nСодержимое файлов\nСодержимое файлов\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevenstr%2Fgh-actions-sbs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevenstr%2Fgh-actions-sbs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevenstr%2Fgh-actions-sbs/lists"}