Архитектура проекта
Проект основан на шаблоне чистой архитектуры, со следующими каталогами и соответствующим разделением обязанностей:

Описание каталогов
1.api: здесь содержится связанный с API код: контроллеры, промежуточное ПО, привязка маршрутов.
2. bootstrap: здесь инициализируются приложение, база данных и переменные среды́.
3. cmd: здесь находится основной файл main приложения, которым запускается сервер.
4. domain: определяются сущности предметной области, объекты значений, интерфейсы.
5. internal: здесь содержатся внутренние служебные функции и пакеты.
6. mongo: связанный с MongoDB код: инициализация базы данных, управление клиентами.
7. repository: реализуется слой доступа к данным для взаимодействия с базой данных.
8. usecase: содержатся бизнес-логика и варианты использования приложения.
.├── Dockerfile # Файл образа
├── api
│ ├── controller # Контроллеры
│ ├── middleware # Промежуточное ПО, JWT-аутентификация
│ └── route # Привязка маршрутов
├── bootstrap
│ ├── app.go # Базовый класс
│ ├── database.go # База данных
│ └── env.go # Конфигурационный класс
├── cmd
│ └── main.go # Команда запуска
├── docker-compose.yaml
├── domain # Слой предметной области
├── go.mod
├── go.sum
├── internal # Внутренние служебные функции, пакеты
│ └── tokenutil
├── mongo # MongoDB
│ └── mongo.go
├── repository # Слой репозитория
└── usecase # Слой вариантов использования
Настройка параметров и запуск проекта
В cmd/main.go
настраивается приложение, конфигурируются переменные среды́ и запускается сервер Gin:
./cmd/main.go
type Application struct {
Env *Env
Mongo mongo.Client
}
type Env struct {
AppEnv string `mapstructure:"APP_ENV"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
ContextTimeout int `mapstructure:"CONTEXT_TIMEOUT"`
DBHost string `mapstructure:"DB_HOST"`
DBPort string `mapstructure:"DB_PORT"`
...
}
func main() {
// «app» — это экземпляр всего приложения для управления ключевыми ресурсами в течение его жизненного цикла
app := bootstrap.App()
// Переменные конфигурации
env := app.Env
// Экземпляр базы данных
db := app.Mongo.Database(env.DBName)
defer app.CloseDBConnection()
timeout := time.Duration(env.ContextTimeout) * time.Second
// Создание экзепляра Gin
gin := gin.Default()
// Привязка маршрутов
route.Setup(env, timeout, db, gin)
// Запуск сервера
gin.Run(env.ServerAddress)
}
Слой интерфейса
В каталоге api/controller
определяется интерфейс для слоя вариантов использования и реализуются методы контроллера:
./api/controller/login_controller.go
// Интерфейс слоя предметной области
type SignupUsecase interface {
Create(c context.Context, user *User) error
GetUserByEmail(c context.Context, email string) (User, error)
CreateAccessToken(user *User, secret string, expiry int) (accessToken string, err error)
CreateRefreshToken(user *User, secret string, expiry int) (refreshToken string, err error)
}
type LoginController struct {
LoginUsecase domain.LoginUsecase
Env *bootstrap.Env
}
func (lc *LoginController) Login(c *gin.Context) {
var request domain.LoginRequest
err := c.ShouldBind(&request)
if err != nil {
c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()})
return
}
user, err := lc.LoginUsecase.GetUserByEmail(c, request.Email)
if err != nil {
c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "User not found with the given email"})
return
}
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)) != nil {
c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "Invalid credentials"})
return
}
accessToken, err := lc.LoginUsecase.CreateAccessToken(&user, lc.Env.AccessTokenSecret, lc.Env.AccessTokenExpiryHour)
if err != nil {
c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
return
}
refreshToken, err := lc.LoginUsecase.CreateRefreshToken(&user, lc.Env.RefreshTokenSecret, lc.Env.RefreshTokenExpiryHour)
if err != nil {
c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()})
return
}
loginResponse := domain.LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}
c.JSON(http.StatusOK, loginResponse)
}
Слой предметной области
В каталоге usecase
реализуется бизнес-логика и варианты использования в соответствии с интерфейсом, определенным в слое интерфейса:
./usecase/login_usecase.go
// Интерфейс слоя предотвращения повреждения данных
type UserRepository interface {
Create(c context.Context, user *User) error
Fetch(c context.Context) ([]User, error)
GetByEmail(c context.Context, email string) (User, error)
GetByID(c context.Context, id string) (User, error)
}
type loginUsecase struct {
userRepository domain.UserRepository
contextTimeout time.Duration
}
func NewLoginUsecase(userRepository domain.UserRepository, timeout time.Duration) domain.LoginUsecase {
return &loginUsecase{
userRepository: userRepository,
contextTimeout: timeout,
}
}
func (lu *loginUsecase) GetUserByEmail(c context.Context, email string) (domain.User, error) {
ctx, cancel := context.WithTimeout(c, lu.contextTimeout)
defer cancel()
return lu.userRepository.GetByEmail(ctx, email)
}
func (lu *loginUsecase) CreateAccessToken(user *domain.User, secret string, expiry int) (accessToken string, err error) {
return tokenutil.CreateAccessToken(user, secret, expiry)
}
func (lu *loginUsecase) CreateRefreshToken(user *domain.User, secret string, expiry int) (refreshToken string, err error) {
return tokenutil.CreateRefreshToken(user, secret, expiry)
}
Слой репозитория
В каталоге repository
реализуется слой доступа к данным для взаимодействия с базой данных MongoDB:
./repository/user_repository.go
// Интерфейс слоя обработки данных
type Database interface {
Collection(string) Collection
Client() Client
}
type userRepository struct {
database mongo.Database
collection string
}
func NewUserRepository(db mongo.Database, collection string) domain.UserRepository {
return &userRepository{
database: db,
collection: collection,
}
}
func (ur *userRepository) Create(c context.Context, user *domain.User) error {
collection := ur.database.Collection(ur.collection)
_, err := collection.InsertOne(c, user)
return err
}
func (ur *userRepository) Fetch(c context.Context) ([]domain.User, error) {
collection := ur.database.Collection(ur.collection)
opts := options.Find().SetProjection(bson.D{{Key: "password", Value: 0}})
cursor, err := collection.Find(c, bson.D{}, opts)
if err != nil {
return nil, err
}
var users []domain.User
err = cursor.All(c, &users)
if users == nil {
return []domain.User{}, err
}
return users, err
}
func (ur *userRepository) GetByEmail(c context.Context, email string) (domain.User, error) {
collection := ur.database.Collection(ur.collection)
var user domain.User
err := collection.FindOne(c, bson.M{"email": email}).Decode(&user)
return user, err
}
func (ur *userRepository) GetByID(c context.Context, id string) (domain.User, error) {
collection := ur.database.Collection(ur.collection)
var user domain.User
idHex, err := primitive.ObjectIDFromHex(id)
if err != nil {
return user, err
}
err = collection.FindOne(c, bson.M{"_id": idHex}).Decode(&user)
return user, err
}
Слой данных
В каталоге mongo
инициализируется клиент MongoDB и предоставляются методы для взаимодействия с базой данных:
./mongo/mongo.go
type mongoDatabase struct {
db *mongo.Database
}
func (md *mongoDatabase) Collection(colName string) Collection {
collection := md.db.Collection(colName)
return &mongoCollection{coll: collection}
}
func (md *mongoDatabase) Client() Client {
client := md.db.Client()
return &mongoClient{cl: client}
}
Синглтон и инкапсуляция
В пакете route
настраивается привязка маршрутов для приложения согласно принципам синглтона и инкапсуляции:
func Setup(env *bootstrap.Env, timeout time.Duration, db mongo.Database, gin *gin.Engine) {
publicRouter := gin.Group("")
// Все общедоступные API
NewSignupRouter(env, timeout, db, publicRouter)
NewLoginRouter(env, timeout, db, publicRouter)
NewRefreshTokenRouter(env, timeout, db, publicRouter)
protectedRouter := gin.Group("")
// Промежуточное ПО для проверки «AccessToken»
protectedRouter.Use(middleware.JwtAuthMiddleware(env.AccessTokenSecret))
// Все приватные API
NewProfileRouter(env, timeout, db, protectedRouter)
NewTaskRouter(env, timeout, db, protectedRouter)
}
Следуя этому шаблону чистой архитектуры, получаем модульное, тестируемое и сопровождаемое приложение Go. Благодаря структуре проекта разделяются обязанности, со временем приложение проще понимать, разрабатывать и масштабировать.
Читайте также:
- Как разместить приложение FastAPI в интернете без удаленного сервера и сложной настройки
- Загрузка файлов в хранилище Cloudflare R2: простое руководство
- Примитивы синхронизации в Go
Читайте нас в Telegram, VK и Дзен
Перевод статьи Bibek Lamichhane: Clean Implementation of Go Project Structure