Task Manager на Golang: полное руководство по созданию

Ошибка cannot find package при первой попытке собрать task manager на Golang почти всегда означает, что проект создан вне модуля — проверьте, выполнена ли команда go mod init в корневой папке. Это типичная точка входа в разработку собственного менеджера задач: прежде чем писать логику, нужно правильно инициализировать окружение.

Task manager на Go — классический учебный и вполне практичный проект: он одновременно затрагивает работу с HTTP-сервером, структурами данных, конкурентностью и хранением состояния. В этой статье разберём архитектуру приложения, примеры кода и типичные ошибки, с которыми сталкиваются разработчики при создании менеджера задач на Go.

Почему Golang подходит для task manager

Язык Go изначально проектировался для серверных приложений, и менеджер задач — идеальная для него задача. Встроенный пакет net/http позволяет поднять REST API без сторонних фреймворков, а горутины дают естественный способ выполнять фоновые задачи: напоминания, периодическую синхронизацию, очистку просроченных записей.

Кроме того, компиляция в единый бинарный файл упрощает развёртывание: готовый task manager можно запустить на любом сервере без установки рантайма. Статическая типизация отлавливает часть ошибок ещё на этапе сборки, что особенно ценно в коде, управляющем состоянием задач.

Структура проекта

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

taskmanager/

├── go.mod

├── main.go

├── internal/

│ ├── task/

│ │ ├── task.go

│ │ └── service.go

│ ├── storage/

│ │ └── memory.go

│ └── handler/

│ └── http.go

└── README.md

  • 📦 internal/task — модель задачи и бизнес-логика (создание, смена статуса, фильтрация)
  • 💾 internal/storage — слой хранения: сначала в памяти, позже база данных
  • 🌐 internal/handler — HTTP-обработчики, маршрутизация запросов
  • 🚀 main.go — точка входа, сборка зависимостей и запуск сервера

Такое разделение позволяет заменить in-memory хранилище на PostgreSQL или SQLite, не переписывая обработчики — достаточно реализовать общий интерфейс.

Модель задачи и базовый код

Ядро приложения — структура Task. Минимальный набор полей: идентификатор, название, статус и время создания. Не перегружайте модель на старте: поля вроде приоритета или дедлайна добавляются позже без боли, а лишние обязательные поля усложняют API.

package task

import "time"

type Status string

const (

StatusTodo Status = "todo"

StatusInProgress Status = "in_progress"

StatusDone Status = "done"

)

type Task struct {

ID int64 `json:"id"`

Title string `json:"title"`

Status Status `json:"status"`

CreatedAt time.Time `json:"created_at"`

}

Обратите внимание на тип Status — объявление отдельного строкового типа с константами защищает от опечаток вроде "donе" с кириллической буквой. Компилятор не пропустит несуществующее значение, если вы везде используете константы.

REST API: обработчики и маршруты

Начиная с версии Go 1.22, стандартный http.ServeMux поддерживает методы и параметры пути, поэтому для простого task manager сторонний роутер не обязателен. Типовой набор эндпоинтов выглядит так:

МетодПутьДействие
GET/tasksСписок всех задач
POST/tasksСоздание задачи
GET/tasks/{id}Получение одной задачи
PATCH/tasks/{id}Обновление статуса или названия
DELETE/tasks/{id}Удаление задачи

Пример регистрации маршрутов и запуска сервера:

mux := http.NewServeMux()

mux.HandleFunc("GET /tasks", h.List)

mux.HandleFunc("POST /tasks", h.Create)

mux.HandleFunc("GET /tasks/{id}", h.Get)

mux.HandleFunc("PATCH /tasks/{id}", h.Update)

mux.HandleFunc("DELETE /tasks/{id}", h.Delete)

log.Fatal(http.ListenAndServe(":8080", mux))

Каждый обработчик должен валидировать входные данные: пустое название задачи, неизвестный статус, нечисловой id. Возвращайте осмысленные HTTP-коды — 400 для невалидного запроса, 404 для отсутствующей задачи, 500 только для действительно внутренних сбоев.

⚠️ Внимание: не доверяйте данным из тела запроса. Декодируйте JSON через json.NewDecoder(r.Body).Decode(&input) с последующей проверкой полей, иначе пустой или битый JSON приведёт к созданию задач с пустыми названиями.
📊 Какой подход к маршрутизации вы используете в Go-проектах?
Стандартный net/http (ServeMux)
Chi
Gorilla Mux
Echo или Gin

Хранение данных: от памяти к базе

На первом этапе храните задачи в памяти — в срезе или map с защитой мьютексом. Это позволяет сосредоточиться на API, не поднимая базу данных. Однако здесь кроется главная ловушка конкурентного доступа: map в Go небезопасен для одновременной записи из нескольких горутин, а HTTP-сервер обрабатывает каждый запрос в отдельной горутине.

type MemoryStorage struct {

mu sync.RWMutex

tasks map[int64]task.Task

nextID int64

}

func (s *MemoryStorage) Add(t task.Task) task.Task {

s.mu.Lock()

defer s.mu.Unlock()

s.nextID++

t.ID = s.nextID

s.tasks[t.ID] = t

return t

}

Для чтения используйте RLock() — это позволяет параллельно выполнять несколько чтений. Когда проект перерастёт in-memory решение, определите интерфейс хранилища и реализуйте его для выбранной базы. Проверить код на гонки помогает флаг -race при запуске тестов и самого приложения.

☑️ Проверка хранилища перед релизом

Выполнено: 0 / 5

Конкурентность: фоновые задачи через горутины

Менеджер задач часто требует фоновой работы: например, периодической пометки просроченных задач или отправки уведомлений. Для этого запускают горутину с time.Ticker, но важно предусмотреть её корректную остановку через контекст.

func StartOverdueChecker(ctx context.Context, s Storage) {

ticker := time.NewTicker(time.Minute)

defer ticker.Stop()

for {

select {

case <-ctx.Done():

return

case <-ticker.C:

s.MarkOverdue()

}

}

}

Без ctx.Done() горутина продолжит работать после завершения main-функции, что приводит к утечкам при перезапусках и в тестах. Для graceful shutdown основного сервера используйте http.Server.Shutdown(ctx) в связке с signal.NotifyContext.

⚠️ Внимание: тикер с интервалом в одну минуту — лишь пример. Подбирайте период под реальную нагрузку: слишком частые проходы по хранилищу на большом числе задач создают лишнюю нагрузку, слишком редкие — запаздывающие статусы.
Что такое graceful shutdown и зачем он нужен

При получении сигнала завершения (SIGINT, SIGTERM) сервер перестаёт принимать новые соединения, но дожидается окончания текущих запросов в течение заданного таймаута. Это предотвращает обрыв записи в хранилище посередине операции и потерю данных пользователей.

Тестирование и типичные ошибки

Тесты для task manager удобно строить на пакете net/http/httptest: он позволяет вызывать обработчики без реального сетевого сервера. Покройте как минимум создание задачи с валидными и невалидными данными, получение несуществующей задачи и конкурентный доступ к хранилищу.

go test -race ./...
  • 🐛 Data race — обращение к map без мьютекса; ловится флагом -race
  • 🔁 Утечка горутин — фоновые воркеры без отмены контекста
  • 📄 Невалидный JSON — отсутствие проверки ошибки после Decode
  • 🔢 Коллизии ID — инкремент счётчика без блокировки

Ещё одна частая проблема — возврат 200 OK на любые запросы, включая ошибки. Клиенты API ориентируются на статус-коды, и маскировка ошибок усложняет отладку на стороне фронтенда или мобильного приложения.

Часто задаваемые вопросы

Нужен ли фреймворк для task manager на Go?

Для учебного и небольшого проекта достаточно стандартной библиотеки: net/http с версии Go 1.22 поддерживает методы и параметры пути. Фреймворки вроде Chi или Echo имеет смысл подключать, когда появляются middleware, сложная маршрутизация и большая команда.

Какую базу данных выбрать для хранения задач?

Для локального приложения удобна SQLite — она не требует отдельного сервера. Для многопользовательского сервиса чаще выбирают PostgreSQL. Начните с интерфейса хранилища, чтобы смена базы не затронула остальной код.

Как проверить API без написания клиента?

Используйте curl из терминала, например: curl -X POST -d '{"title":"Купить молоко"}' localhost:8080/tasks. Также подойдут Postman, Insomnia или расширения REST-клиентов для редактора кода.

Почему приложение падает с ошибкой concurrent map writes?

Это следствие одновременной записи в map из нескольких горутин без синхронизации. Оберните все операции чтения и записи в sync.RWMutex либо используйте sync.Map, если профиль нагрузки ему соответствует.

Как упаковать task manager для развёртывания?

Выполните go build — получите единый исполняемый файл без внешних зависимостей. Для контейнеризации обычно используют многоступенчатый Dockerfile на базе минимального образа, но конкретную конфигурацию подбирайте под свою инфраструктуру.