Backend
Эта страница описывает соглашения для внесения вклада в Go-бэкенд. Если вы ещё не читали, сначала прочитайте страницу Архитектура.
Добавление нового доменного объекта
Следуйте этой последовательности при добавлении нового объекта (например, document):
- Домен —
internal/domain/<name>/entity.go: структура объекта, перечисления, ошибки - Интерфейс репозитория — добавить в
internal/repository/interfaces.go - Реализация репозитория —
internal/repository/<name>_repository.go - Сценарий использования —
internal/usecase/<group>/<name>_usecase.go+ файл типов - Обработчик —
internal/handler/<name>_handler.go - Маршруты — подключить в
internal/server/server.go - DI — подключить репозиторий + сценарий использования в
internal/app/container.go - Миграция —
migrations/<seq>_create_<name>s_table.up.sql - Моки — добавить интерфейс в директиву
go:generateвinternal/repository/interfaces.go, запуститьjust generate-mocks - Тесты — юнит-тест сценария использования с моками; интеграционный тест репозитория с testcontainers
Соглашения по именованию
- Имена пакетов: короткие, строчные, в единственном числе —
user,project,support - Имена файлов:
<entity>_entity.go,<entity>_repository.go,<entity>_usecase.go,<entity>_handler.go - ID объектов:
ulid.ULIDв структурах,stringв DTO (через.String()) - Неэкспортируемые структуры репозитория:
type userRepository struct { db *sqlx.DB } - Конструктор:
func NewUserRepository(db *sqlx.DB) repository.UserRepository
Шаблон обработчика
Обработчики тонкие. Они привязывают, вызывают, отвечают — и больше ничего.
func (h *PersonHandler) Create(c *gin.Context) {
var req CreatePersonRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
projectID := c.Param("project_id")
userID, _ := middleware.UserIDFrom(c)
out, err := h.personUC.Create(c.Request.Context(), usecase.CreatePersonInput{
ProjectID: projectID,
CreatedBy: userID.String(),
// ... поля из req
})
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, out)
}Шаблон сценария использования
Сценарии использования координируют репозитории. Они не содержат HTTP или SQL кода.
type CreatePersonInput struct {
ProjectID string
FirstName string
LastName string
// ...
}
type CreatePersonOutput struct {
PersonID string `json:"person_id"`
}
func (uc *PersonUseCase) Create(ctx context.Context, in CreatePersonInput) (*CreatePersonOutput, error) {
person := &domain.Person{
ID: ulid.New(),
ProjectID: in.ProjectID,
FirstName: in.FirstName,
// ...
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := uc.repo.Create(ctx, person); err != nil {
return nil, err
}
return &CreatePersonOutput{PersonID: person.ID.String()}, nil
}Обработка ошибок
Доменные ошибки определяются в internal/domain/<name>/errors.go:
var (
ErrPersonNotFound = errors.New("person not found")
ErrPersonExists = errors.New("person already exists")
)Обработчик сопоставляет доменные ошибки с HTTP-кодами статуса в internal/handler/errors.go. Добавляйте туда новые ошибки по мере необходимости. Не возвращайте сырые ошибки базы данных из сценариев использования.
Миграции
Только вперёд — без файлов .down.sql. Шаблон имени файла:
<seq>_<description>.up.sqlГде <seq> — шестизначное число с ведущими нулями. Создать с помощью:
observer migrate create <description>
# или
just migrate-create <description>Никогда не изменяйте применённую миграцию. Вместо этого создайте новую.
Внедрение зависимостей
Всё подключение происходит в internal/app/container.go. Шаблон:
// 1. Создать репозиторий
personRepo := repository.NewPersonRepository(c.db.GetDB())
// 2. Создать сценарий использования
personUC := projectUC.NewPersonUseCase(personRepo, ...)
// 3. Сохранить в контейнере
c.PersonUC = personUCЗатем в internal/server/server.go внедрить в обработчик:
personHandler := handler.NewPersonHandler(container.PersonUC)Стиль кода
- Никаких декоративных разделителей комментариев (
//-----,//=====) - Docstring только для экспортируемых символов
- Сложная логика: предпочитайте диаграмму Mermaid в README модуля вместо встроенных комментариев
- Форматирование
gofmtобязательно — запускайтеjust fmtперед коммитом - Линтинг:
just lint(golangci-lint)