HTML формы и Go

32 minute read

Перевод статьи "HTML Forms and Go".

Это небольшой отрывок из моей книги "Go Web Programming", в котором рассказывается о использовании языка программирования Go для обработки данных из HTML форм. Это звучит достаточно тривиально, но, как и многое в веб-программировании (и программировании в принципе), эти тривиальные вещи часто оказываются камнем преткновения.

Перед тем как мы разберемся с обработкой данных из форм на стороне сервера, давайте чуть более внимательно посмотрим на их HTML описание. Чаще всего, POST реквесты приходят из HTML форм которые выглядят аналогично этому примеру:

<form action="/process" method="post">
<input type="text" name="first_name"/>
<input type="text" name="last_name"/>
<input type="submit"/>
</form>

Внутри HTML формы мы можем разместить различные элементы, такие как текстовые поля ввода(text), текстовые области(textarea), переключатели(radiobutton), поля для множ. выбора(checkboxes), поля для загрузки файлов и многое другое. Эти элементы позволяют пользователям ввести данные, которые будут отправлены на сервер. Отправка происходит после того, как пользователь нажмет кнопку, или некоторым другим способом сработает триггер отправки формы.

Мы знаем, что данные отправлены на сервер в виде HTTP POST запроса и размещены в теле самого запроса. Но как они будут отформатированы? Данные из HTML форм всегда образуют пары ключ-значение, но в каком формате эти пары ключ-значение отправляются в запросе? Для нас очень важно знать это, потому что нам придется разбирать эти данные из POST запроса на сервере и извлекать пары ключ-значение. Формат отправляемых данных зависит от типа контента в HTML форме. Он задается с помощью атрибута enctype в таком виде:

<form action="/process" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="first_name"/>
<input type="text" name="last_name"/>
<input type="submit"/>
</form>

Значение по умолчанию для enctype это application/x-www-form-urlencoded, но браузеры должны поддерживать как минимум application/x-www-form-urlencoded и multipart/form-data (HTML5 еще требуется поддержка text/plain).

Если мы укажем значение аттрибута enctype как application/x-www-form-urlencoded, то браузер преобразует данные из формы в длинную строку запроса в которой пары ключ-значение разделены амперсандами (&) и имя с значением разделены символом равно (=), что выглядит также, как строка URL с параметрами, собственно, отсюда и название. Другими словами, тело HTTP запроса выглядит как-то так:

first_name=sau%20sheong&last_name=chang

Если установить значение аттрибута enctype как multipart/form-data, то каждая пара ключ-значение преобразуется в часть MIME сообщения, каждая с указанием типа и места размещения для контента. Для примера, данные из нашей подопытной формы будут выглядеть так:

------WebKitFormBoundaryMPNjKpeO9cLiocMw
Content-Disposition: form-data; name="first_name"

sau sheong
------WebKitFormBoundaryMPNjKpeO9cLiocMw
Content-Disposition: form-data; name="last_name"

chang
------WebKitFormBoundaryMPNjKpeO9cLiocMw--

Когда стоит использовать тот или другой способ передачи? Если мы отправляем простые текстовые данные, то лучше выбрать URL форматирование, так как разбирать такие данные проще. Если же необходимо отправить большое количество данных, особенно если отправляем файлы, то следует использовать multipart-MIME формат. В таком случае, мы можем указать кодировку base64 для бинарных файлов, чтоб отправить их как текст.

До сих пор мы говорили только о POST запросах, как насчет GET? HTML формы позволяют указывать значения атрибута method как POST так и GET, поэтому следующая форма также валидна:

<form action="/process" method="get">
<input type="text" name="first_name"/>
<input type="text" name="last_name"/>
<input type="submit"/>
</form>

В таком случае нет никакого тела запроса (Это специфично для GET запросов), все данные устанавливаются в URL как пары ключ-значение.

Теперь мы знаем в каком виде данные из HTML форм отправляются на сервер, можем вернутся к серверу и используя пакет net/http обработать запрос.

Form

Один из способов извлечения данных из HTTP запроса это выдернуть данные из тела запроса и/или из URL в сыром виде, и самим распарсить эти данные. К счастью, в этом нет необходимости, потому что пакет net/http предоставляет нам полный набор функций, хотя и не всегда очевидно называющихся, для работы с формами. Давайте, для затравки, рассмотрим пару часто используемых функций.

Функции из Request, который позволяют нам извлечь данные формы из тела запроса и/или из URL, вращаются вокруг полей Form, PostForm и MultipartForm. Как вы помните, данные это пары ключ-значение(которые мы получаем из POST запроса в том или ином виде). Основной алгоритм работы с этими данными такой:

  • Вызов ParseForm или ParseMultipartForm для разбора запроса
  • Обращения к Form, PostForm или MultipartForm соответственно

Давайте напишем немного кода.

package main

import (
    "fmt"
    "net/http"
)

func process(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    fmt.Fprintln(w, r.Form)
}

func main() {
    server := http.Server{
    Addr: "127.0.0.1:8080",
}
http.HandleFunc("/process", process)
    server.ListenAndServe()
}

Сфокусируем внимание на двух строчках этого кода:

r.ParseForm()
fmt.Fprintln(w, r.Form)

Как уже было сказано выше, прежде всего нам нужно распарсить запрос с использованием ParseForm и после этого можем обращаться к полю Form

Давайте посмотрим, как выглядит клиент для вызова нашего серверного кода. Создадим минимальную HTML форму для отправки запроса. Поместите код в файл client.html.

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Go Web Programming</title>
</head>
<body>
<form action="http://127.0.0.1:8080/process?hello=world&thread=123" 
method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="hello" value="sau sheong"/>
<input type="text" name="post" value="456"/>
<input type="submit"/>
</form>
</body>
</html>

В этой форме мы:

  • Отправляем на сервер данные по URL http://localhost:8080/process?hello=world&thread=123 методом POST
  • Указываем тип контента в поле enctype как application/x-www-form-urlencoded
  • Отправляем на сервер две пары ключ-значение - hello=sau sheong и post=456

Обратите внимание, у нас есть 2 значения для ключа hello. Одно из них это world в URL, другое это sau sheong в HTML форме.

Откройте в браузере файл client.html напрямую (вам не нужен сервер для этого) и кликните по кнопке отправки. В браузере вы должны увидеть:

map[thread:[123] hello:[sau sheong world] post:[456]]

Это строка показывает содержимое поля Form после POST запроса и его парсинга. Form это обычный мап со строками в качестве ключей и слайсами строк для значений. Стоит упомянуть, что это не сортированный map и вы всегда будете получать поля в случайном порядке. Тем не менее, мы получаем все значения из строки запроса hello=world и thread=123, а также значения из формы hello=sau sheong и post=456. Значения из URL декодированы (это видно по пробелу между sau and sheong).

PostForm

Конечно, вы можете обратиться только к одному ключу, например post. Для это надо указать этот ключ Form["post"] и вы получите слайс с одним элементом - [456]. Если и в форме и в URL есть одинаковые ключи, они оба попадут в слайс , значение из формы будет считаться более приоритетным и это значение будет расположено перед значением из URL.

Что если нам нужны только пары ключ-значение из формы, а параметры из URL нужно игнорировать? Для этого у нас есть метод PostForm, который дает доступ только к значениям из формы. Если в нашем примере заменить строку r.Form на r.PostForm, то вывод приложения будет таким:

map[post:[456] hello:[sau sheong]]

Мы используем application/x-www-form-urlencoded для указания типа контента. Что случится, если будем использовать multipart/form-data? Внесите необходимые изменения в наш html файл и верните назад r.Form. Смотрим, что получилось:

map[hello:[world] thread:[123]]

Что мы видим? В r.Form попали только значения из URL и никаких пар ключ-значения из формы. Это происходит потому что PostForm поддерживает только application/x-www-form-urlencoded. Для получения multipart данных из тела запроса нам нужно использовать MultipartForm.

MultipartForm

Вместо вызова ParseForm а затем обращения к Form нам необходимо использовать ParseMultipartForm и обращаться к полю MultipartForm. Метод ParseMultipartForm при необходимости сам вызывает ParseForm.

r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm)

Нам нужно сообщить ParseMultipartForm, как много данных необходимо извлечь из multipart формы(в байтах). Теперь можем посмотреть результат:

&{map[hello:[sau sheong] post:[456]] map[]}

Тут мы видим пары ключ-значения из формы, но нет никаких значений из URL. Это означает, что MultipartForm содержит только данные из формы. Обратите внимание, что полученное значение это структура с двумя мапами, а не с одним. Первый мап со строковыми ключами и слайсами строк для значений, а второй мап пустой. В этом втором мапе ключи также строковые, а вот значения уже файлы.

Существует еще один набор функций, который позволяет нам работать с парами ключ-значение намного проще. Метод FormValue позволяет обратиться к значению по ключу, аналогично обращению к Form, но нам не нужно перед этим вызывать ParseForm или ParseMultipartForm, FormValue сделает это за нас.

Это означает, что если мы чуть-чуть подправим наш пример:

fmt.Fprintln(w, r.FormValue("hello"))

И укажем параметр формы enctype как application/x-www-form-urlencoded, то результат будет таким:

sau sheong

Мы получили только sau sheong, потому что FormValue возвращает только первый элемент из слайса, даже если реально у нас есть оба значения в поле Form. Что бы убедиться в этом, давайте добавим еще пару строчек кода:

fmt.Fprintln(w, r.FormValue("hello"))
fmt.Fprintln(w, r.Form)

И увидим вот такую картину:

sau sheong
map[post:[456] hello:[sau sheong world] thread:[123]]

Функция PostFormValue работает аналогично, только не для Form, а для PostForm. Давайте внесем некоторые изменения в наш для использования функции PostFormValue:

fmt.Fprintln(w, r.PostFormValue("hello"))
fmt.Fprintln(w, r.PostForm)

В результате получем что то такое:

sau sheong
map[hello:[sau sheong] post:[456]]

Как можно заметить, тут вывелись только пары ключ-значение указанные в форме.

Обе функции FormValue и PostFormValue самостоятельно вызывают вызывают ParseMultipartForm и нам не нужно этого делать в ручную, но тут кроется небольшая ловушка и вам нужно быть осторожным(по крайней мере для Go 1.4). Если мы установим значение атрибута enctype как multipart/form-data и попробуем получить значения с использованием FormValue или PostFormValue, то ничего не получится, даже если использовать MultipartForm!

Чтобы понять, о чем идет речь, давайте еще немного поэкспериментируем с нашим примером:

fmt.Fprintln(w, "(1)", r.FormValue("hello"))
fmt.Fprintln(w, "(2)", r.PostFormValue("hello"))
fmt.Fprintln(w, "(3)", r.PostForm)
fmt.Fprintln(w, "(4)", r.MultipartForm)

При установленном значении enctype как multipart/form-data результаты будут такими:

(1) world
(2)
(3) map[]
(4) &{map[hello:[sau sheong] post:[456]] map[]}

Первая строчка результата - это значение параметра hello из URL, не из формы. Вторая и третья строка демонстрируют, что мы не сможем получить пары ключ-значения из формы. Так происходит, потому что FormValue и PostFormValue работают с Form и PostForm, никак не касаясь MultipartForm. Последняя строка - демонстрация того, что функция ParseMultipartForm была вызвана и сделала возможным доступ к параметрам через MultipartForm

Мы можем резюмировать все что было описано в этой статье в виде небольшой таблицы, в которой показано, как функции для работы с данными запроса отличаются друг от друга.