Ошибка 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 приведёт к созданию задач с пустыми названиями.
Хранение данных: от памяти к базе
На первом этапе храните задачи в памяти — в срезе или 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 при запуске тестов и самого приложения.
☑️ Проверка хранилища перед релизом
Конкурентность: фоновые задачи через горутины
Менеджер задач часто требует фоновой работы: например, периодической пометки просроченных задач или отправки уведомлений. Для этого запускают горутину с 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 на базе минимального образа, но конкретную конфигурацию подбирайте под свою инфраструктуру.