kata academy

Архитектура на Go для новичков

Как не писать монолит, даже если это твой первый проект

Время чтения: 4 минуты
Хочешь кодить как босс?
Заполняй форму и начни свой путь в IT прямо сейчас!
Когда новичок начинает свой первый проект на Go, соблазн велик: собрать всё в одном файле main.go, подключить несколько библиотек и радостно закодить бизнес-логику прямо рядом с HTTP-хендлерами и доступом к базе. В статье разобрали, почему так делать не надо, как избежать классического монолита и сразу привить себе архитектурные привычки для масштабирования проектов.
IT-калькулятор зарплат
Узнай свою рыночную зарплату за 1 минуту!
Почему монолит — ловушка для новичка
Монолит сам по себе не всегда зло, но его главная проблема — хаос. В одном коде перемешиваются:
  • обработка HTTP-запросов,
  • бизнес-логика,
  • работа с базой данных,
  • утилитарные функции.
В итоге изменить или протестировать что-то точечно становится сложно. А на Go это особенно критично: язык быстрый, компилируется мгновенно, и тебе хочется двигаться так же быстро.
Минимальная архитектура для старта
Новичку не нужно погружаться в сложную архитектуру, достаточно разделить код по слоям, чтобы проект оставался читаемым.

Базовая структура проекта
/cmd/app/ # Точка входа в приложение (main.go)
/internal/
/api/ # HTTP-хендлеры, роутинг
/service/ # Бизнес-логика
/repository/ # Доступ к БД, внешним сервисам
/model/ # Общие структуры данных (DTO, сущности)
/config/ # Конфигурация приложения

Как это работает
  • api — только про HTTP (или gRPC). Здесь нет логики, кроме вызова сервисов.
  • service — сердце приложения, где описана бизнес-логика.
  • repository — слой для хранения и извлечения данных. Может быть Postgres, Redis или даже in-memory map.
  • model — общие структуры, которые используют все слои.
  • config — настройки приложения из переменных окружения.
Пример архитектуры: ToDo-лист без монолита
В примере мы показали, как разделить слои.

Модель
package model
type Task struct {
    ID    int           `json:"id"`
    Title string    `json:"title"`
    Done  bool   `json:"done"`
}

Репозиторий
package repository
import ( "context" "example.com/project/internal/model" )

type TaskRepository interface {
    Save(ctx context.Context, task model.Task) (*model.Task, error)
    FindAll(ctx context.Context) ([]model.Task, error)
    FindByID(ctx context.Context, id int) (*model.Task, error)
}

Можно сделать реализацию в памяти, а позже заменить на Postgres — интерфейс останется тем же.

Сервис
package service
import (
"context"
"errors"
"example.com/project/internal/model"
"example.com/project/internal/repository"
)
type TaskService struct {
repo repository.TaskRepository
}
func NewTaskService(r repository.TaskRepository) *TaskService {
    return &TaskService{repo: r}
}
func (s *TaskService) CreateTask(ctx context.Context, title string) (*model.Task, error) {
    if title == "" {
        return nil, errors.New("название задачи не может быть пустым")
    }
    task := model.Task{Title: title}
    return s.repo.Save(ctx, task)
}

API
package api
import (
    "encoding/json"
    "net/http"
    "example.com/project/internal/service"
)
type Handler struct {
    taskService *service.TaskService
}
func NewHandler(s *service.TaskService) *Handler {
    return &Handler{taskService: s}
}
func (h *Handler) CreateTask(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var input struct {
        Title string `json:"title"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "Некорректный JSON", http.StatusBadRequest)
        return
    }
    
    task, err := h.taskService.CreateTask(r.Context(), input.Title)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(task)
}
Что даёт такая архитектура
  • Легко тестировать сервисы без запуска HTTP-сервера.
  • Можно менять базу данных, не переписывая бизнес-логику.
  • Можно масштабировать проект в сторону микросервисов.
Твой первый оффер: зарплата от 120 000 рублей! Пройди курс по Go-разработке с гарантией трудоустройства и получи такое предложение от работодателя! Основная оплата курса проходит уже после выхода на работу, ты платишь за полученный результат!

Ошибки новичков в архитектуре на Go
1) Всё в main.go

Плохой пример кода
func main() {
db, _ := sql.Open("postgres", "...")
http.HandleFunc("/tasks", func(w http.ResponseWriter, r *http.Request) {
rows, _ := db.Query("SELECT id, title FROM tasks")
// логика прямо тут...
 })
    http.ListenAndServe(":8080", nil)
}

Рабочий пример кода
func main() {
    db, err := initDB()
    if err != nil {
        log.Fatal("Ошибка подключения к БД:", err)
    }
    repo := repository.NewTaskRepo(db)
    svc := service.NewTaskService(repo)
    handler := api.NewHandler(svc)
    mux := http.NewServeMux()
    mux.HandleFunc("/tasks", handler.CreateTask)
    
    log.Println("Сервер запущен на :8080")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal("Ошибка запуска сервера:", err)
    }
}

2) Смешение уровней

Плохой пример: SQL-запрос в хендлере
func CreateTask(w http.ResponseWriter, r *http.Request) {
db.Exec("INSERT INTO tasks (title) VALUES ($1)", "Test")
}

Рабочий пример: вызов бизнес-логики:
func (h *Handler) CreateTask(w http.ResponseWriter, r *http.Request) {
    h.taskService.CreateTask(r.Context(), "Test")
}

3) Отсутствие интерфейсов

Плохой пример: сервис жёстко зависит от Postgres
type TaskService struct {
db *sql.DB
}

Рабочий пример: сервис работает через интерфейс
type TaskRepository interface { 
Save(ctx context.Context, task Task) (*Task, error) 
}
type TaskService struct {
repo TaskRepository
}

4) Слишком сложная архитектура «с порога»

Плохой пример: начинаете со сложных паттернов (DI-контейнеры, CQRS, hexagonal).
Рабочий пример: простая структура папок /api, /service, /repository, без оверхеда.

5) Хардкод конфигураций

Плохой пример кода
db, _ := sql.Open("postgres", "postgres://user:pass@localhost/db")

Рабочий пример кода
dsn := os.Getenv("DB_DSN")
if dsn == "" {
    log.Fatal("Переменная DB_DSN не установлена")
}
db, err := sql.Open("postgres", dsn)
if err != nil {
    log.Fatal("Ошибка подключения к БД:", err)
}

Так приложение запускается в любой среде без правки кода: локально, Docker, облако.

6) Игнорирование context.Context

Плохой пример кода
rows, _ := db.Query("SELECT * FROM tasks")

Рабочий пример кода
func (r *TaskRepo) FindAll(ctx context.Context) ([]model.Task, error) {
    rows, err := r.db.QueryContext(ctx, "SELECT * FROM tasks")
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    // ... обработка результатов
}

7) Запутанные зависимости

Плохой пример
  • api импортирует repository
  • repository зачем-то импортирует api
  • компилятор ругается на циклические зависимости
Рабочий пример
  • api вызывает service
  • service использует repository
  • зависимости только «вниз», без циклов.
Итог
Go хорош тем, что позволяет писать просто и быстро. Но простота не должна означать «свалку всего подряд». Даже первый проект стоит организовать так, чтобы завтра не пришлось его переписывать.

Важно: примеры в статье упрощены для понимания. В production-коде обязательно добавьте полноценную обработку ошибок, логирование и тесты.

Запомни правило: чёткие границы между слоями — залог успешного Go-проекта. Начни с минимальной архитектуры, и ты сразу будешь впереди других новичков!

А если хочешь освоить профессию Go-разработчика с гарантией трудоустройства, поступай в Kata Academy. Курс проходит с личной поддержкой ментора. Узнай подробнее на сайте.

Статьи для старта в IT

Истории наших выпускников

Стань тем, кто задаёт тон в IT!
Подпишись на нашу рассылку и первым получай статьи по Java, JavaScript, Golang и QA. Позволь себе быть экспертом!