Pull to refresh

Веб-разработка на Go

Website development *
Translation
Original author: Go developers
Статья основана на codelab с сайта Go, но не ограничивается им. По ходу прочтения статьи можно будет узнать о структурах данных Go, динамических массивах, использовании библиотек http, template, regexp для создания веб-приложения, шаблонизации и фильтрации ввода, соответственно.
image
Для понимания статьи необходимо немножко уметь программировать, не пугаться слов 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, т.е. будет выполнен эскейпинг и, например > заменится &gt;. Это позволит корректно отображать данные в форме.

Теперь вызова 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 в коде пришлось разбить, дабы не выводить из себя хабрапарсер.
Tags:
Hubs:
Total votes 56: ↑54 and ↓2 +52
Views 23K
Comments Comments 37