html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

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

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

материал с habrahabr.ru

1

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • wawan93
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции