Статья основана на 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 в коде пришлось разбить, дабы не выводить из себя хабрапарсер.