HTML формы и Go
Перевод статьи "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
Мы можем резюмировать все что было описано в этой статье в виде небольшой таблицы, в которой показано, как функции для работы с данными запроса отличаются друг от друга.