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 - Тести — unit-тест варіанту використання з моками; інтеграційний тест репозиторію з 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)