Перейти к содержанию

Ошибки формы и повторный ввод

Форма из предыдущего урока уже создаёт карточку, но простой ответ «заполните поля» помогает не во всех случаях. Человек может ввести длинное название, смешать алфавиты артикула или отправить отрицательное количество. Задача этого шага — сохранить его значения, объяснить причину отказа и оставить хранилище прежним. Ошибка формы является ожидаемым результатом пользовательского ввода, поэтому её нужно отличать от поломки SQL или шаблона.

Используем прежние поля SKU, название, место и остаток. Добавляем ограничения длины и допустимого алфавита артикула. Артикул состоит из заглавных латинских букв, цифр и дефиса. Правило выбрано для небольшого корпоративного справочника; оно не претендует на универсальный формат всех производителей. Если реальные артикулы содержат другие символы, предметный договор нужно изменить осознанно, а не тайно отбрасывать знаки при отправке формы.

Значение до решения о записи

Функция parseItem возвращает не только сообщение ошибки, но и модель с уже прочитанными строками. В обработчике она передаётся обратно в Page.Form. Благодаря этому название и место не исчезают после отказа. Внутренний ID пока не назначен, поскольку запись не произошла. Полезно разделять «значения, предложенные пользователем» и «состояние, подтверждённое базой»: одинаковая структура не делает их автоматически одним предметным событием.

Ниже показан фрагмент проверки из полного main.go состояния lesson-06. Функция выполняется до INSERT; SQL и успешное перенаправление остаются прежними.

func parseItem(r *http.Request)(Item,string){
 if err:=r.ParseForm();err!=nil{return Item{},"Форма не прочитана"}
 p:=Item{SKU:strings.TrimSpace(r.PostForm.Get("sku")),Name:strings.TrimSpace(r.PostForm.Get("name")),Location:strings.TrimSpace(r.PostForm.Get("location"))}
 q,err:=strconv.Atoi(r.PostForm.Get("quantity"));p.Quantity=q
 if err!=nil||q<0||q>1000000{return p,"Остаток должен быть целым числом от 0 до 1000000"}
 if p.SKU==""||p.Name==""||p.Location==""{return p,"Заполните артикул, название и место"}
 if len(p.SKU)>32||len(p.Name)>150||len(p.Location)>80{return p,"Превышена длина поля"};for _,v:=range p.SKU{if !(v>='A'&&v<='Z'||v>='0'&&v<='9'||v=='-'){return p,"Артикул: латинские заглавные буквы, цифры и дефис"}}
 return p,""
}

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

Особенность текущего примера: значение Quantity хранится числом. Если пользователь отправил буквы вместо числа, точное исходное содержимое этого поля не сохраняется в структуре Item; в форме появится ноль, а сообщение объяснит, что нужен целый остаток. Название, артикул и место сохраняются. Для точного восстановления любого сырого ввода понадобилась бы отдельная строковая модель формы, а не попытка заставить числовое предметное поле хранить ошибочный текст.

Длина строки и число символов

В Go вызов len для строки считает байты. Поэтому ограничение 150 для названия в нашем исходнике является пределом UTF-8-байтов, а не обещанием 150 русских букв. Это существенная деталь: короткое русское название обычно занимает больше байтов, чем число видимых символов. Мы фиксируем техническую границу честно. Если интерфейс должен ограничивать именно количество кодовых точек, следует использовать другой подсчёт и согласовать его с подписью поля.

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

Артикул разбирается по символам, а сравнения с диапазонами A–Z и 0–9 задают точный разрешённый набор. Кириллическая буква, визуально похожая на латинскую, не проходит проверку. Мы не пытаемся угадать намерение и автоматически заменить её. Иначе два внешне похожих артикула могут неожиданно объединиться, а исходный код поставщика потеряется. Для справочника полезнее сообщить конкретное правило и попросить явное исправление.

Статус 422 и представление ошибки

Ответ с ошибкой формы имеет статус 422. Он означает, что обращение прочитано, но предложенные значения не приняты по установленному договору. Это отличается от 400 для неразбираемого запроса и от 500 для внутренней неисправности. Обычный браузер всё равно может показать HTML-тело такого ответа. Когда добавим htmx, отдельно разрешим замену представления для выбранных статусов: библиотека не обязана вставлять любые ошибочные ответы автоматически.

Сообщение выводится через HTML-шаблон как текст и имеет role="alert". Сервер не собирает разметку сообщения из SKU или SQL-ошибки. Так сохраняется безопасность отображения, введённая в третьем уроке. Если добавить подробный текст базы прямо в карточку, человек получит лишние внутренние сведения, а программист случайно свяжет интерфейс с формулировкой конкретного драйвера. Предметные сообщения лучше удерживать под контролем приложения.

Рассмотрим артикул мышь-001 с правильным названием и остатком 8. Ожидается отказ алфавита без INSERT. Затем пользователь меняет SKU на MOUSE-001; те же название и место остаются в форме, и успешный запрос проходит обычный цикл записи. Если такой SKU уже существует, проверка уникальности возвращает другую причину. Изменение одного условия показывает, что правила независимы и каждое решает свой вид неопределённости.

Дублирование части ограничений в HTML помогает человеку раньше увидеть проблему, но сервер сохраняет решение. Атрибуты required, min и max можно обойти прямым запросом, изменить в инструментах браузера или потерять в другом клиенте. Не нужно считать пользователя злоумышленником для объяснения этой границы: новый мобильный интерфейс тоже может ошибиться. Предметное хранилище принимает только те данные, которые сервер проверил самостоятельно.

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

Важно не исправлять артикул незаметно при сохранении. Автоматическое преобразование в верхний регистр иногда удобно, однако должно быть частью объявленного договора. Иначе человек вводит одно значение, а позже ищет другое и не понимает причины различия. Наша проверка возвращает правило вместо скрытой замены. Такой подход оставляет контроль за пользователем и делает ошибку воспроизводимой: один и тот же ввод получает одно и то же решение независимо от внешнего вида формы и способа отправки запроса.

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-06. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.