Restful сервис на Go (Golang)

go restful service

Всем привет! Для тех кто только хочет стать гофером буду рад помочь данной статьей. Код представленный в данной статье не является обязательной практикой а лишь подсказывает и дает пример с чего начать, но он еще далек от идеала).

Введение в архитектуру 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?

  1. Фабрика — это шаблон проектирования, который упрощает создание объектов. Это полезно, когда нужно создать объект с набором параметров или когда сама логика создания сложная.
  2. 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")
}

Что мы сделали?

  1. Фабрики: Мы создали фабрики для репозиториев и сервисов, чтобы централизовать логику их создания. Это делает наш код более гибким и управляемым.
  2. Dependency Injection: Мы внедряем зависимости в контроллеры через конструкторы. Теперь зависимости не создаются внутри объектов, а передаются извне, что упрощает тестирование и поддержку кода.

Итог

Теперь у нас более модульная и гибкая структура RESTful сервиса на Go с использованием фреймворка Gin. Мы добавили фабрики и внедрение зависимостей, что значительно упростило поддержку кода. Наш сервис готов к дальнейшему масштабированию!

Если статья оказалась тебе полезной то поделись ею с другими, мне приятно а тебе почет и уважение).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *