JSON HTTP API: Task API с валидацией, единообразными ошибками и потокобезопасным хранилищем
Содержание курса
Switch по r.Method, ответ 405 и маршрутизация /tasks и /tasks/{id}
Регистрируем два паттерна:
mux := http.NewServeMux()
mux.HandleFunc("/tasks", store.handleTasks)
mux.HandleFunc("/tasks/", store.handleTaskByID)
Паттерн /tasks без слеша на конце матчит только точный путь /tasks. Паттерн /tasks/ с завершающим слешем — это префикс, он поймает /tasks/1, /tasks/42 и всё остальное, начинающееся с /tasks/. Так стандартный ServeMux разграничивает «список» и «конкретный элемент».
Внутри каждого обработчика читаем r.Method:
func (s *Store) handleTasks(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
s.listTasks(w, r)
case http.MethodPost:
s.createTask(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
Почему не положиться на ServeMux с method-qualified паттернами вроде "GET /tasks"? Потому что когда ServeMux сам формирует 405, он отдаёт текстовый ответ без гарантии application/json и нашего поля error. Клиент нашего API ожидает единый формат ошибок — и default-ветка с writeError это обеспечивает. Собственный switch выбран не из-за ограничений Go, а ради контракта.
Для /tasks/ нужно ещё извлечь ID из пути:
func (s *Store) handleTaskByID(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/tasks/")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
writeError(w, http.StatusNotFound, "task not found")
return
}
switch r.Method {
case http.MethodPatch:
s.updateTask(w, r, id)
case http.MethodDelete:
s.deleteTask(w, r, id)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
strings.TrimPrefix отрезает /tasks/ — остаётся строка с числом. Если Atoi возвращает ошибку или число не положительное, это уже некорректный запрос, и 404 здесь уместнее 400: такого ресурса не существует.
