Всем привет! Для тех кто только хочет стать гофером буду рад помочь данной статьей. Код представленный в данной статье не является обязательной практикой а лишь подсказывает и дает пример с чего начать, но он еще далек от идеала).
Введение в архитектуру RESTful сервисов
Прежде чем мы погрузимся в код, давайте разберемся, что такое RESTful сервис. REST (Representational State Transfer) — это архитектурный стиль для взаимодействия между клиентами и серверами через HTTP. RESTful сервисы используют стандартные методы HTTP, такие как GET, POST, PUT, DELETE, для работы с ресурсами.
Go (или Golang) идеально подходит для создания таких сервисов, благодаря своей простоте, производительности и встроенной поддержке для работы с HTTP. А Gin — это легковесный веб-фреймворк, который позволяет быстро разрабатывать приложения с минимальной задержкой. Он быстрый и удобный, и я обещаю, что мы не будем жалеть о выборе.
Структура проекта
Я люблю порядок, поэтому наша структура проекта будет выглядеть примерно так:
/restful-service │── .env │── /cmd │ └── main.go │── /config │ └── config.go └── /internal │── /handlers │ └── user_handler.go │── /models │ └── user.go │── /services │ └── user_service.go │── /repositories │ └── user_repository.go └── /middlewares └── auth_middleware.go
Каждый слой имеет свою ответственность:
- Handlers: здесь мы определяем конечные точки (endpoints), которые будут обрабатывать HTTP-запросы.
- Models: представление данных, т.е. наши объекты (например, пользователь).
- Services: бизнес-логика (все хитрые манипуляции с данными).
- Repositories: взаимодействие с базой данных.
- Middlewares: вспомогательные функции (например, авторизация).
- Config: конфигурация приложения.
Давайте разбираться с каждым слоем по очереди.
Настройка проекта и базовые зависимости
Для начала создадим новый проект:
mkdir restful-service cd restful-service go mod init restful-service
Теперь установим Gin:
go get -u github.com/gin-gonic/gin
И не забудем базу данных, например PostgreSQL (чтобы было посложнее):
go get -u github.com/lib/pq
Ну, и еще кое-что для работы с переменными окружения (настраивать конфигурацию вручную — это уже прошлый век):
go get -u github.com/joho/godotenv
Теперь у нас есть всё для старта!
Конфигурация (config/config.go)
Начнем с базовой настройки, загрузим наши переменные окружения. Создадим в корневой папке файл .env
:
DB_HOST=localhost DB_USER=postgres DB_PASSWORD=secret DB_NAME=restful_service DB_PORT=5432
Теперь создадим config.go
:
package config import ( "fmt" "os" "github.com/joho/godotenv" ) type Config struct { DBHost string DBUser string DBPassword string DBName string DBPort string } func LoadConfig() Config { err := godotenv.Load() if err != nil { panic("Error loading .env file") } config := Config{ DBHost: os.Getenv("DB_HOST"), DBUser: os.Getenv("DB_USER"), DBPassword: os.Getenv("DB_PASSWORD"), DBName: os.Getenv("DB_NAME"), DBPort: os.Getenv("DB_PORT"), } return config } func GetDatabaseURL(config Config) string { return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", config.DBUser, config.DBPassword, config.DBHost, config.DBPort, config.DBName) }
Просто и понятно — загружаем переменные и создаем строку подключения к базе данных.
Модели (models/user.go)
Давайте создадим первую модель — пользователя. Пользователи — это основа любого хорошего сервиса (ну, почти любого).
package models type User struct { ID uint `json:"id"` Name string `json:"name"` Email string `json:"email"` Password string `json:"-"` }
Мы просто описываем структуру данных, с которой будем работать. Ничего сложного!
Репозиторий (repositories/user_repository.go)
Теперь нам нужен слой, который будет взаимодействовать с базой данных. Здесь будет происходить магия запросов:
package repositories import ( "database/sql" "restful-service/models" ) type UserRepository struct { DB *sql.DB } func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{DB: db} } func (r *UserRepository) GetAll() ([]models.User, error) { rows, err := r.DB.Query("SELECT id, name, email FROM users") if err != nil { return nil, err } defer rows.Close() var users []models.User for rows.Next() { var user models.User err := rows.Scan(&user.ID, &user.Name, &user.Email) if err != nil { return nil, err } users = append(users, user) } return users, nil } func (r *UserRepository) Create(user models.User) error { _, err := r.DB.Exec("INSERT INTO users (name, email, password) VALUES ($1, $2, $3)", user.Name, user.Email, user.Password) return err }
Здесь у нас есть методы для получения всех пользователей и создания нового пользователя. Всё, как в лучших домах Лондона и Парижа!
Сервис (services/user_service.go)
Сервисный слой — это то, где будет происходить вся логика работы с данными. Именно здесь пользователи будут обрабатываться перед отправкой в базу данных или на клиент.
package services import ( "restful-service/models" "restful-service/repositories" ) type UserService struct { UserRepository *repositories.UserRepository } func NewUserService(userRepo *repositories.UserRepository) *UserService { return &UserService{UserRepository: userRepo} } func (s *UserService) GetAllUsers() ([]models.User, error) { return s.UserRepository.GetAll() } func (s *UserService) CreateUser(user models.User) error { // Здесь можно добавить валидацию или бизнес-логику return s.UserRepository.Create(user) }
Мы можем добавить сюда дополнительную бизнес-логику, но пока оставим всё просто. Наш сервис просто обрабатывает запросы к репозиторию.
Обработчики или контроллеры (handlers/user_handler.go)
Наконец-то мы добрались до хендлеров — той части кода, с которой взаимодействует внешний мир. Здесь мы будем обрабатывать HTTP-запросы и возвращать ответы.
package handlers import ( "net/http" "restful-service/models" "restful-service/services" "github.com/gin-gonic/gin" ) type UserHandler struct { UserService *services.UserService } func NewUserHandler(userService *services.UserService) *UserHandler { return &UserHandler{UserService: userService} } func (ctrl *UserHandler) GetUsers(c *gin.Context) { users, err := ctrl.UserService.GetAllUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to fetch users"}) return } c.JSON(http.StatusOK, users) } func (ctrl *UserHandler) CreateUser(c *gin.Context) { var user models.User if err := c.ShouldBindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) return } err := ctrl.UserService.CreateUser(user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to create user"}) return } c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) }
Теперь у нас есть два эндпоинта: один для получения списка пользователей, другой для создания нового.
Маршрутизация и запуск приложения (main.go)
Мы почти у цели! Теперь свяжем все части вместе и запустим наш сервис.
package main import ( "database/sql" "restful-service/config" "restful-service/handlers" "restful-service/repositories" "restful-service/services" "github.com/gin-gonic/gin" _ "github.com/lib/pq" ) func main() { // Загружаем конфигурацию cfg := config.LoadConfig() // Подключаемся к базе данных db, err := sql.Open("postgres", config.GetDatabaseURL(cfg)) if err != nil { panic(err) } defer db.Close() // Инициализируем зависимости userRepo := repositories.NewUserRepository(db) userService := services.NewUserService(userRepo) userHandler := controllers.NewUserHandler(userService) // Создаем Gin роутер router := gin.Default() // Определяем маршруты router.GET("/users", userHandler.GetUsers) router.POST("/users", userHandler.CreateUser) // Запускаем сервер router.Run(":8080") }
Теперь наш сервис готов! Мы можем запустить его:
go run cmd/main.go
Проверка сервиса
Теперь можно протестировать наш RESTful сервис. Для этого используйте curl
или Postman:
- Получить список пользователей:
curl http://localhost:8080/users
Создать нового пользователя:
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{"name": "Misha", "email": "misha@example.com", "password": "password123"}'
Давайте усложним наш проект, добавив фабрики и внедрение зависимостей (Dependency Injection). Это поможет лучше структурировать код, улучшить его тестируемость и гибкость. Фабрики позволят нам динамически создавать экземпляры классов (или структур в Go), а внедрение зависимостей сделает код менее зависимым от конкретных реализаций классов.
Что такое фабрики и DI?
- Фабрика — это шаблон проектирования, который упрощает создание объектов. Это полезно, когда нужно создать объект с набором параметров или когда сама логика создания сложная.
- Dependency Injection (DI) — это подход, при котором зависимости (например, объекты, от которых зависит класс) предоставляются извне, а не создаются внутри класса напрямую. Это делает код более гибким и его легче тестировать.
Теперь давайте применим эти концепции к нашему проекту.
1. Фабрика для создания сервисов и репозиториев
Мы создадим фабрики для репозиториев и сервисов, чтобы облегчить их создание и централизовать логику. Например, фабрика для репозиториев будет отвечать за создание экземпляров репозиториев с подключением к базе данных.
Фабрика для репозиториев (repositories/repository_factory.go
)
package repositories import ( "database/sql" ) type RepositoryFactory struct { DB *sql.DB } func NewRepositoryFactory(db *sql.DB) *RepositoryFactory { return &RepositoryFactory{DB: db} } func (f *RepositoryFactory) CreateUserRepository() *UserRepository { return NewUserRepository(f.DB) }
Теперь фабрика создаёт экземпляры репозиториев с уже переданным подключением к базе данных.
Фабрика для сервисов (services/service_factory.go
)
package services import ( "restful-service/repositories" ) type ServiceFactory struct { RepoFactory *repositories.RepositoryFactory } func NewServiceFactory(repoFactory *repositories.RepositoryFactory) *ServiceFactory { return &ServiceFactory{RepoFactory: repoFactory} } func (f *ServiceFactory) CreateUserService() *UserService { userRepo := f.RepoFactory.CreateUserRepository() return NewUserService(userRepo) }
Здесь фабрика создаёт сервисы, передавая им необходимые репозитории. Внедрение зависимостей осуществляется через фабрику, что делает код более гибким.
2. Внедрение зависимостей через конструктор (Dependency Injection)
Теперь мы можем перейти к внедрению зависимостей через конструктор. Этот подход будет применён к контроллерам. Мы изменим наш main.go
, чтобы использовать фабрики и внедрение зависимостей.
3. Изменения в контроллерах
Обработчики пользователя (handlers/user_handler.go
)
Хэндлер останется почти неизменным, однако теперь мы будем передавать сервисы через конструктор, что делает их зависимости явными.
package handlers import ( "net/http" "restful-service/models" "restful-service/services" "github.com/gin-gonic/gin" ) type UserHandler struct { UserService *services.UserService } func NewUserHandler(userService *services.UserService) *UserHandler { return &UserHandler{UserService: userService} } func (ctrl *UserHandler) GetUsers(c *gin.Context) { users, err := ctrl.UserService.GetAllUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to fetch users"}) return } c.JSON(http.StatusOK, users) } func (ctrl *UserHandler) CreateUser(c *gin.Context) { var user models.User if err := c.ShouldBindJSON(&user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) return } err := ctrl.UserService.CreateUser(user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to create user"}) return } c.JSON(http.StatusCreated, gin.H{"message": "User created successfully"}) }
4. Основной файл main.go
с фабриками и DI
Теперь мы внесем изменения в main.go
, чтобы внедрить зависимостей через фабрики:
package main import ( "database/sql" "restful-service/config" "restful-service/handlers" "restful-service/repositories" "restful-service/services" "github.com/gin-gonic/gin" _ "github.com/lib/pq" ) func main() { // Загружаем конфигурацию cfg := config.LoadConfig() // Подключаемся к базе данных db, err := sql.Open("postgres", config.GetDatabaseURL(cfg)) if err != nil { panic(err) } defer db.Close() // Инициализируем фабрики repoFactory := repositories.NewRepositoryFactory(db) serviceFactory := services.NewServiceFactory(repoFactory) // Создаем сервисы и контроллеры с внедрением зависимостей userService := serviceFactory.CreateUserService() userHandler := handlers.NewUserHandler(userService) // Создаем Gin роутер router := gin.Default() // Определяем маршруты router.GET("/users", userHandler.GetUsers) router.POST("/users", userHandler.CreateUser) // Запускаем сервер router.Run(":8080") }
Что мы сделали?
- Фабрики: Мы создали фабрики для репозиториев и сервисов, чтобы централизовать логику их создания. Это делает наш код более гибким и управляемым.
- Dependency Injection: Мы внедряем зависимости в контроллеры через конструкторы. Теперь зависимости не создаются внутри объектов, а передаются извне, что упрощает тестирование и поддержку кода.
Итог
Теперь у нас более модульная и гибкая структура RESTful сервиса на Go с использованием фреймворка Gin. Мы добавили фабрики и внедрение зависимостей, что значительно упростило поддержку кода. Наш сервис готов к дальнейшему масштабированию!
Если статья оказалась тебе полезной то поделись ею с другими, мне приятно а тебе почет и уважение).