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) {
req, ok := bindJSON[CreatePersonRequest](c)
if !ok {
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)
}bindJSON[T] визначена в internal/handler/errors.go. Вона декодує тіло запиту та записує відповідь 400 при помилці, повертаючи false, щоб обробник міг одразу завершитися.
Шаблон варіанту використання
Варіанти використання координують репозиторії. Вони не містять 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)