Статья основана на codelab с сайта Go, но не ограничивается им. По ходу прочтения статьи можно будет узнать о структурах данных Go, динамических массивах, использовании библиотек http, template, regexp для создания веб-приложения, шаблонизации и фильтрации ввода, соответственно.

Для понимания статьи необходимо немножко уметь программировать, не пугаться слов unix и веб. Основы языка будут изложены в статье.
Итак, первое, что необходимо для работы с Go — Linux, FreeBSD (OS X), хотя MinGW под Windows тоже сойдёт.
Go необходимо установить, для этого нужно выполнить примерно следующее (инструкции приведены для систем с dpkg):
Если всё хорошо, можно добавить в ~/.bashrc или ~/.profile следующее:
При повторном входе в шелл и вызове компилятора (8g для i386 или 6g для amd64, далее будет 8g) мы получим справочное сообщение:
Это означает, что Go у нас установлен и работает, можно перейти непосредственно к приложению.
Создадим директорию для приложения:
Создадим текстовым редактором (биндинги для vim и emacs) файл wiki.go со следующим содержимым:
По названию понятно, что наше приложение позволит нам редактировать и сохранять страницы.
Данный код импортирует библиотеки fmt, ioutil и os из стандартной библиотеки Go. Позже мы добавим некоторые другие библиотеки.
Определим несколько структур данных. Вики — набор связанных страниц, обладающих телом и заголовком. Соответствующая структура данных будет иметь два поля:
Тип данных []byte — это срез (slice) типа byte, аналог динамического массива (подробнее: Effective Go) Тело статьи сохраняется в []byte, а не в string для удобства работы со стандартными библиотеками.
Структура данных описывает то, как данные хранятся в памяти. Но что, если нужно сохранить данные надолго? Реализуем метод save для сохранения на диск:
Сигнатура данной функции гласит: «Это метод save, применимый к указателю на page, без параметров, возвращающий значение типа os.Error.»
Данный метод сохранит текст в файл. Для простоты будем считать, что заголовок является именем файла. Если это кажется недостаточно безопасным, можно импортировать crypto/md5 и использовать вызов md5.New(filename).
Возвращаемое значение будет иметь тип os.Error, соответственно возвращаемому значению вызова WriteFile (функция стандартной библиотеки для записи среза в файл). Это сделано для того, чтобы в дальнейшем можно было обработать ошибку сохранения в файл. Если не возникнет проблем, page.save() вернёт нам nil (нулевое значение для указателей, интерфейсов и некоторых других типов).
Восьмеричная константа 0600, третий параметр вызова WriteFile, указывает, что файл сохраняется с правами чтения и записи только для текущего пользователя.
Также было бы интересно загружать страницу:
Эта функция получает имя файла из заголовка, читает содержимое в переменную типа page и возвращает указатель на неё.
Функции в Go могут возвращать несколько значений. Функция стандартной библиотеки io.ReadFile возвращает []byte и os.Error. В функции loadPage ошибки ещё не обрабатываются: символ подчёркивания означает «не сохранять это значение».
Что происходит, если ReadFile возвращает ошибку? Например, страницы с таким заголовком нет. Это существенная ошибка, её нельзя игнорировать. Пусть наша функция тоже возвращает два значения: *page и os.Error.
Теперь можно проверить значение второго параметра: если оно равно nil, то страница успешно загрузилась. В противном случае, это будет значение типа os.Error.
Итак, у нас есть структура данных и методы загрузки-выгрузки. Пора проверить, как это работает:
После компиляции и исполнения этого кода, файл TestPage.txt будет содержать значение p1->body. После этого данное значение загрузится в переменную p2 и выведется на экран.
Для сборки и запуска программы необходимо выполнить следующее:
Самый простой веб-сервер на Go выглядит так:
Функция main вызывает http.HandleFunc, которая сообщает библиотеке http, что всевозможные запросы ("/") обрабатываются функцией handler.
Следующим вызовом http.ListenAndServe, мы определяем, что мы хотим обрабатывать запросы на всех интерфейсах на порту 8080 (":8080"). Второй параметр пока нам не требуется. Программа будет работать в таком режиме до принудительного завершения.
Функция-обработчик имеет тип http.HandlerFunc. Она принимает в качестве параметров http.ResponseWriter и указатель на http.Request.
Значение типа http.ResponseWriter формирует ответ http; записывая туда данные (посредством вызова Fprintf) мы возвращаем пользователю содержимое страницы.
Структура данных http.Request представляет собой запрос пользователя. Строка r.URL.Path — путь. Суффикс [1:] означает «получить срез Path (подстроку) с первого символа и до конца», т.е., удалить ведущий слэш.
Запустив браузер и открыв URL http://localhost:8080/habrahabr, мы увидим на странице желаемое:
Импортируем библиотеку http:
Создадим обработчик для отображения статьи:
Во-первых, данная функция извлекает заголовок из r.URL.Path, компоненты пути заданного URL. Глобальная константа lenPath — длина префикса "/view/" в пути, обозначающего просмотр текста статьи в нашей системе. Выделяется подстрока [lenPath:], т.е., заголовок статьи, исключается префикс.
Функция загружает данные, дополняя их простыми html-тегами и пишет в w, параметр типа http.ResponseWriter.
Вновь используется _ для игнорирования возвращаемого значения типа os.Error. Это сделано для простоты и вообще так делать нехорошо. Ниже будет указано, как обрабатывать такие ошибки правильно.
Для вызова данного обработчика, напишем функцию main, инициализирующую http соответствующим viewHandler для обработки запросов по пути /view/.
Создадим тестовую страницу (в файле test.txt), скомпилируем код и попробуем выдать страницу:
Пока работает наш сервер, по адресу http://localhost:8080/view/test будет доступна страница с заголовком «test», содержащая слова «Hello world».
Что это за вики без возможности правки страниц? Создадим два новых обработчика: editHandler для отображения формы редактирования и saveHandler для сохранения полученных данных.
Сперва, добавим их в main():
Функция editHandler загружает страницу (или создаёт пустую структуру, если такой страницы нет), и отображает форму:
Функция работает хорошо и правильно, но выглядит некрасиво. Причина в хардкоде html, но это исправимо.
Библиотека template входит в стандартную библиотеку Go. Мы можем использовать шаблоны для хранения разметки html вне кода, чтобы можно было менять разметку без перекомпиляции.
Сперва, импортируем template:
Создадим шаблон формы в файле edit.html, со следующим содержимым:
Изменим editHandler таким образом, чтобы использовать шаблон:
Метод template.ParseFile прочтёт файл edit.html и выдаст значение типа *template.Template.
Метод t.Execute заменяет все вхождения {title} и {body} на значения p.title и p.body, и выводит полученный html в переменную типа http.ResponseWriter.
Заметьте, в шаблоне встречалась конструкция {body|html}. Она означает, что параметр будет отформатирован для вывода в html, т.е. будет выполнен эскейпинг и, например > заменится >. Это позволит корректно отображать данные в форме.
Теперь вызова fmt.Sprintf в программе нет, можно убрать fmt из импорта.
Создадим также шаблон для отображения страницы, view.html:
Изменим viewHandler соответствующим образом:
Отметим, что код для вызова шаблонов почти не отличается в том и в другом случае. Избавимся от дублирования, вынеся этот код в отдельную функцию:
Теперь обработчики короче и проще.
Что случится при переходе по адресу /view/APageThatDoesntExist? Программа упадёт. А всё потому, что мы не обработали второе значение, возвращаемое loadPage. Если страница не существует, мы будем перенаправлять пользователя на страницу создания новой статьи:
Функция http.Redirect добавляет HTTP статус http.StatusFound (302) и заголовок Location к ответу HTTP.
Функция saveHandler обрабатывает данные с формы.
Создаётся новая страница с выбранным залоговком и телом. Метод save() сохраняет данные в файл, клиент перенаправляется на страницу /view/.
Значение, возвр��щаемое FormValue, имеет тип string. Для сохранения в структуру страницы мы конвертируем его в []byte записью []byte(body).
Мы игнорируем ошибки в нашей программе в нескольких местах. Это приводит к тому, что программа падает при возникновении ошибки, поэтому лучше возвращать пользователю сообщение об ошибке, сервер же продолжит работу.
Сперва, добавим обработку ошибок в renderTemplate:
Функция http.Error отправляет выбранный статус HTTP (в данном случае «Internal Server Error») и возвращает сообщение об ошибке.
Сделаем аналогичную правку в saveHandler:
Любые ошибки, возникающие в p.save() будут переданы пользователю.
Наш код недостаточно эффективен: renderTemplate вызывает ParseFile при каждом рендеринге странице. Гораздо лучше вызывать ParseFile единожды для каждого шаблона при запуске программы, сохраняя полученные значения типа *Template в структуру для дальнейшего использования.
Сперва, создадим карту templates, в которой сохраним значения *Template, ключом в карте будет имя шаблона:
Далее мы создадим функцию инициализации, которую вызовем перед main(). Функция template.MustParseFile — обёртка для ParseFile, не возвращающая код ошибки, вместо этого она паникует. Действительно, такое поведение допустимо для программы, ведь неизвестно, как обрабатывать некорректный шаблон.
Цикл for используется с конструкцией range и обрабатывает заданные шаблоны.
Далее, изменим функцию renderTemplate так, чтобы она вызывал метод Execute соответствующего шаблона:
Как уже было отмечено, в нашей программе есть серьёзная ошибки безопасности. Вместо названия можно передать произвольный путь. Добавим проверку регулярным выраженим.
Импортируем библиотеку regexp. Создадим глобальную переменную, в которую сохраним наше РВ:
Фунция regexp.MustCompile скомпилирует регулярное выражение и вернёт regexp.Regexp. MustCompile, как и template.MustParseFile, отличается от Compile тем, что паникует в случае ошибки, тогда как Compile возвращает код ошибки.
Теперь, построим функцию, извлекающую заголовок из URL, и проверяющую его РВ titleValidator:
Если заголовок корректен, вместе с ним вернётся значение nil. В противном случае, пользователю будет выведено «404 Not Found», а обработчику будет возвращена ошибка.
Добавим вызов getTitle в каждый из обработчиков:
Проверка ошибок и возвратов порождает довольно однообразный код, хорошо бы и его написать всего один раз. Это возможно, если, например, обернуть функции, возвращающие ошибки в соответствующий вызов, в этом нам и помогут функциональные типы.
Перепишем обработчики, добавив параметр title:
Определим теперь функцию-обёртку, принимающую тип функции определённой выше, и возвращающей http.HandlerFunc (чтобы передавать её в http.HandleFunc):
Возвращаемая функция является замыканием, т.к. использует значения, определённые вне её (в данном случае это переменная fn, обработчик).
Перенесём теперь сюда код из getTitle:
Замыкание, возвращаемое makeHandler — функция, принимающая параметры типа http.ResponseWriter и http.Request (т.е., функция типа http.HandlerFunc). Это замыкание извлекает заголовок из URL и проверяет его РВ titleValidator. Если заголовок неверен, на ResponseWriter будет передана ошибка (вызов http.NotFound). В противном случае будет вызван соответствующий обработчик fn.
Добавим вызов обёртки в функцию main():
Finally we remove the calls to getTitle from the handler functions, making them much simpler:
Вот что должно получиться в итоге
Пересоберём код и запустим наше приложение:
По адресу http://localhost:8080/view/ANewPage будет страничка с формой. Можно будет сохранить страницу и перейти к ней.
Примечание. textarea в коде пришлось разбить, дабы не выводить из себя хабрапарсер.

Для понимания статьи необходимо немножко уметь программировать, не пугаться слов unix и веб. Основы языка будут изложены в статье.
Установка Go
Итак, первое, что необходимо для работы с Go — Linux, FreeBSD (OS X), хотя MinGW под Windows тоже сойдёт.
Go необходимо установить, для этого нужно выполнить примерно следующее (инструкции приведены для систем с dpkg):
$ sudo apt-get install bison ed gawk gcc libc6-dev make # инструменты для сборки $ sudo apt-get install python-setuptools python-dev build-essential # инструменты для сборки и установки $ sudo easy_install mercurial # если вдруг его ещё нет, из репозитория (через apt) лучше не ставить $ hg clone -r release https://go.googlecode.com/hg/ go $ cd go/src $ ./all.bash
Если всё хорошо, можно добавить в ~/.bashrc или ~/.profile следующее:
export GOROOT=$HOME/go export GOARCH=386 # или amd64, в зависимости от архитектуры ОС export GOOS=linux export GOBIN=$GOROOT/bin PATH=$PATH:$GOBIN
При повторном входе в шелл и вызове компилятора (8g для i386 или 6g для amd64, далее будет 8g) мы получим справочное сообщение:
gc: usage 8g [flags] file.go...
Это означает, что Go у нас установлен и работает, можно перейти непосредственно к приложению.
Начало. Структуры данных
Создадим директорию для приложения:
$ mkdir ~/gowiki $ cd ~/gowiki
Создадим текстовым редактором (биндинги для vim и emacs) файл wiki.go со следующим содержимым:
package main import ( "fmt" "io/ioutil" "os" )
По названию понятно, что наше приложение позволит нам редактировать и сохранять страницы.
Данный код импортирует библиотеки fmt, ioutil и os из стандартной библиотеки Go. Позже мы добавим некоторые другие библиотеки.
Определим несколько структур данных. Вики — набор связанных страниц, обладающих телом и заголовком. Соответствующая структура данных будет иметь два поля:
type page struct {
title string
body []byte
}Тип данных []byte — это срез (slice) типа byte, аналог динамического массива (подробнее: Effective Go) Тело статьи сохраняется в []byte, а не в string для удобства работы со стандартными библиотеками.
Структура данных описывает то, как данные хранятся в памяти. Но что, если нужно сохранить данные надолго? Реализуем метод save для сохранения на диск:
func (p *page) save() os.Error {
filename := p.title + ".txt"
return ioutil.WriteFile(filename, p.body, 0600)
}Сигнатура данной функции гласит: «Это метод save, применимый к указателю на page, без параметров, возвращающий значение типа os.Error.»
Данный метод сохранит текст в файл. Для простоты будем считать, что заголовок является именем файла. Если это кажется недостаточно безопасным, можно импортировать crypto/md5 и использовать вызов md5.New(filename).
Возвращаемое значение будет иметь тип os.Error, соответственно возвращаемому значению вызова WriteFile (функция стандартной библиотеки для записи среза в файл). Это сделано для того, чтобы в дальнейшем можно было обработать ошибку сохранения в файл. Если не возникнет проблем, page.save() вернёт нам nil (нулевое значение для указателей, интерфейсов и некоторых других типов).
Восьмеричная константа 0600, третий параметр вызова WriteFile, указывает, что файл сохраняется с правами чтения и записи только для текущего пользователя.
Также было бы интересно загружать страницу:
func loadPage(title string) *page {
filename := title + ".txt"
body, _ := ioutil.ReadFile(filename)
return &page{title: title, body: body}
}Эта функция получает имя файла из заголовка, читает содержимое в переменную типа page и возвращает указатель на неё.
Функции в Go могут возвращать несколько значений. Функция стандартной библиотеки io.ReadFile возвращает []byte и os.Error. В функции loadPage ошибки ещё не обрабатываются: символ подчёркивания означает «не сохранять это значение».
Что происходит, если ReadFile возвращает ошибку? Например, страницы с таким заголовком нет. Это существенная ошибка, её нельзя игнорировать. Пусть наша функция тоже возвращает два значения: *page и os.Error.
func loadPage(title string) (*page, os.Error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &page{title: title, body: body}, nil
}Теперь можно проверить значение второго параметра: если оно равно nil, то страница успешно загрузилась. В противном случае, это будет значение типа os.Error.
Итак, у нас есть структура данных и методы загрузки-выгрузки. Пора проверить, как это работает:
func main() {
p1 := &page{title: "TestPage", body: []byte("Тестовая страница.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.body))
}После компиляции и исполнения этого кода, файл TestPage.txt будет содержать значение p1->body. После этого данное значение загрузится в переменную p2 и выведется на экран.
Для сборки и запуска программы необходимо выполнить следующее:
$ 8g wiki.go $ 8l wiki.8 $ ./8.out This is a sample page.
Библиотека http
Самый простой веб-сервер на Go выглядит так:
package main
import (
"fmt"
"http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привет %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}Функция main вызывает http.HandleFunc, которая сообщает библиотеке http, что всевозможные запросы ("/") обрабатываются функцией handler.
Следующим вызовом http.ListenAndServe, мы определяем, что мы хотим обрабатывать запросы на всех интерфейсах на порту 8080 (":8080"). Второй параметр пока нам не требуется. Программа будет работать в таком режиме до принудительного завершения.
Функция-обработчик имеет тип http.HandlerFunc. Она принимает в качестве параметров http.ResponseWriter и указатель на http.Request.
Значение типа http.ResponseWriter формирует ответ http; записывая туда данные (посредством вызова Fprintf) мы возвращаем пользователю содержимое страницы.
Структура данных http.Request представляет собой запрос пользователя. Строка r.URL.Path — путь. Суффикс [1:] означает «получить срез Path (подстроку) с первого символа и до конца», т.е., удалить ведущий слэш.
Запустив браузер и открыв URL http://localhost:8080/habrahabr, мы увидим на странице желаемое:
Привет, habrahabr!
Использование http для выдачи страниц
Импортируем библиотеку http:
import ( "fmt" "http" "io/ioutil" "os" )
Создадим обработчик для отображения статьи:
const lenPath = len("/view/")
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.title, p.body)
}Во-первых, данная функция извлекает заголовок из r.URL.Path, компоненты пути заданного URL. Глобальная константа lenPath — длина префикса "/view/" в пути, обозначающего просмотр текста статьи в нашей системе. Выделяется подстрока [lenPath:], т.е., заголовок статьи, исключается префикс.
Функция загружает данные, дополняя их простыми html-тегами и пишет в w, параметр типа http.ResponseWriter.
Вновь используется _ для игнорирования возвращаемого значения типа os.Error. Это сделано для простоты и вообще так делать нехорошо. Ниже будет указано, как обрабатывать такие ошибки правильно.
Для вызова данного обработчика, напишем функцию main, инициализирующую http соответствующим viewHandler для обработки запросов по пути /view/.
func main() {
http.HandleFunc("/view/", viewHandler)
http.ListenAndServe(":8080", nil)
}Создадим тестовую страницу (в файле test.txt), скомпилируем код и попробуем выдать страницу:
$ echo "Hello world" > test.txt $ 8g wiki.go $ 8l wiki.8 $ ./8.out
Пока работает наш сервер, по адресу http://localhost:8080/view/test будет доступна страница с заголовком «test», содержащая слова «Hello world».
Изменение страниц
Что это за вики без возможности правки страниц? Создадим два новых обработчика: editHandler для отображения формы редактирования и saveHandler для сохранения полученных данных.
Сперва, добавим их в main():
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
http.ListenAndServe(":8080", nil)
}Функция editHandler загружает страницу (или создаёт пустую структуру, если такой страницы нет), и отображает форму:
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<text area name=\"body\">%s</text area>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.title, p.title, p.body)
}Функция работает хорошо и правильно, но выглядит некрасиво. Причина в хардкоде html, но это исправимо.
Библиотека template
Библиотека template входит в стандартную библиотеку Go. Мы можем использовать шаблоны для хранения разметки html вне кода, чтобы можно было менять разметку без перекомпиляции.
Сперва, импортируем template:
import ( "http" "io/ioutil" "os" "template" )
Создадим шаблон формы в файле edit.html, со следующим содержимым:
<h1>Editing {title}</h1>
<form action="/save/{title}" method="POST">
<div><text area name="body" rows="20" cols="80">{body|html}</text area></div>
<div><input type="submit" value="Save"></div>
</form>Изменим editHandler таким образом, чтобы использовать шаблон:
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
t, _ := template.ParseFile("edit.html", nil)
t.Execute(p, w)
}Метод template.ParseFile прочтёт файл edit.html и выдаст значение типа *template.Template.
Метод t.Execute заменяет все вхождения {title} и {body} на значения p.title и p.body, и выводит полученный html в переменную типа http.ResponseWriter.
Заметьте, в шаблоне встречалась конструкция {body|html}. Она означает, что параметр будет отформатирован для вывода в html, т.е. будет выполнен эскейпинг и, например > заменится >. Это позволит корректно отображать данные в форме.
Теперь вызова fmt.Sprintf в программе нет, можно убрать fmt из импорта.
Создадим также шаблон для отображения страницы, view.html:
<h1>{title}</h1>
<p>[<a href="/edit/{title}">edit</a>]</p>
<div>{body}</div>Изменим viewHandler соответствующим образом:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
t, _ := template.ParseFile("view.html", nil)
t.Execute(p, w)
}Отметим, что код для вызова шаблонов почти не отличается в том и в другом случае. Избавимся от дублирования, вынеся этот код в отдельную функцию:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(w, "edit", p)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
t, _ := template.ParseFile(tmpl+".html", nil)
t.Execute(p, w)
}Теперь обработчики короче и проще.
Обработка отсутствующих страниц
Что случится при переходе по адресу /view/APageThatDoesntExist? Программа упадёт. А всё потому, что мы не обработали второе значение, возвращаемое loadPage. Если страница не существует, мы будем перенаправлять пользователя на страницу создания новой статьи:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}Функция http.Redirect добавляет HTTP статус http.StatusFound (302) и заголовок Location к ответу HTTP.
Сохранение страниц
Функция saveHandler обрабатывает данные с формы.
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}Создаётся новая страница с выбранным залоговком и телом. Метод save() сохраняет данные в файл, клиент перенаправляется на страницу /view/.
Значение, возвр��щаемое FormValue, имеет тип string. Для сохранения в структуру страницы мы конвертируем его в []byte записью []byte(body).
Обработка ошибок
Мы игнорируем ошибки в нашей программе в нескольких местах. Это приводит к тому, что программа падает при возникновении ошибки, поэтому лучше возвращать пользователю сообщение об ошибке, сервер же продолжит работу.
Сперва, добавим обработку ошибок в renderTemplate:
func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
t, err := template.ParseFile(tmpl+".html", nil)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
return
}
err = t.Execute(p, w)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
}
}Функция http.Error отправляет выбранный статус HTTP (в данном случае «Internal Server Error») и возвращает сообщение об ошибке.
Сделаем аналогичную правку в saveHandler:
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}Любые ошибки, возникающие в p.save() будут переданы пользователю.
Кэширование шаблонов
Наш код недостаточно эффективен: renderTemplate вызывает ParseFile при каждом рендеринге странице. Гораздо лучше вызывать ParseFile единожды для каждого шаблона при запуске программы, сохраняя полученные значения типа *Template в структуру для дальнейшего использования.
Сперва, создадим карту templates, в которой сохраним значения *Template, ключом в карте будет имя шаблона:
var templates = make(map[string]*template.Template)
Далее мы создадим функцию инициализации, которую вызовем перед main(). Функция template.MustParseFile — обёртка для ParseFile, не возвращающая код ошибки, вместо этого она паникует. Действительно, такое поведение допустимо для программы, ведь неизвестно, как обрабатывать некорректный шаблон.
func init() { for _, tmpl := range []string{"edit", "view"} { templates[tmpl] = template.MustParseFile(tmpl+".html", nil) } }Цикл for используется с конструкцией range и обрабатывает заданные шаблоны.
Далее, изменим функцию renderTemplate так, чтобы она вызывал метод Execute соответствующего шаблона:
func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
err := templates[tmpl].Execute(p, w)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
}
}Валидация
Как уже было отмечено, в нашей программе есть серьёзная ошибки безопасности. Вместо названия можно передать произвольный путь. Добавим проверку регулярным выраженим.
Импортируем библиотеку regexp. Создадим глобальную переменную, в которую сохраним наше РВ:
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")Фунция regexp.MustCompile скомпилирует регулярное выражение и вернёт regexp.Regexp. MustCompile, как и template.MustParseFile, отличается от Compile тем, что паникует в случае ошибки, тогда как Compile возвращает код ошибки.
Теперь, построим функцию, извлекающую заголовок из URL, и проверяющую его РВ titleValidator:
func getTitle(w http.ResponseWriter, r *http.Request) (title string, err os.Error) {
title = r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
err = os.NewError("Invalid Page Title")
}
return
}Если заголовок корректен, вместе с ним вернётся значение nil. В противном случае, пользователю будет выведено «404 Not Found», а обработчику будет возвращена ошибка.
Добавим вызов getTitle в каждый из обработчиков:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err = p.save()
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}Функциональные типы и замыкания
Проверка ошибок и возвратов порождает довольно однообразный код, хорошо бы и его написать всего один раз. Это возможно, если, например, обернуть функции, возвращающие ошибки в соответствующий вызов, в этом нам и помогут функциональные типы.
Перепишем обработчики, добавив параметр title:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) func editHandler(w http.ResponseWriter, r *http.Request, title string) func saveHandler(w http.ResponseWriter, r *http.Request, title string)
Определим теперь функцию-обёртку, принимающую тип функции определённой выше, и возвращающей http.HandlerFunc (чтобы передавать её в http.HandleFunc):
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Here we will extract the page title from the Request,
// and call the provided handler 'fn'
}
}Возвращаемая функция является замыканием, т.к. использует значения, определённые вне её (в данном случае это переменная fn, обработчик).
Перенесём теперь сюда код из getTitle:
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
return
}
fn(w, r, title)
}
}Замыкание, возвращаемое makeHandler — функция, принимающая параметры типа http.ResponseWriter и http.Request (т.е., функция типа http.HandlerFunc). Это замыкание извлекает заголовок из URL и проверяет его РВ titleValidator. Если заголовок неверен, на ResponseWriter будет передана ошибка (вызов http.NotFound). В противном случае будет вызван соответствующий обработчик fn.
Добавим вызов обёртки в функцию main():
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
http.ListenAndServe(":8080", nil)
}Finally we remove the calls to getTitle from the handler functions, making them much simpler:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &page{title: title, body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}Вот что должно получиться в итоге
Пересоберём код и запустим наше приложение:
$ 8g wiki.go $ 8l wiki.8 $ ./8.out
По адресу http://localhost:8080/view/ANewPage будет страничка с формой. Можно будет сохранить страницу и перейти к ней.
Примечание. textarea в коде пришлось разбить, дабы не выводить из себя хабрапарсер.
