веб приложение на golang
Основы веб-приложений на Golang
Теперь, когда все правильно настроено, давайте создадим первую версию нашего веб-приложения на Golang.
Начнем с трех основных пунктов:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
На заметку: Функция-обработчик home является обычной функцией с двумя параметрами. Параметр http.ResponseWriter предоставляет методы для объединения HTTP ответа и возвращение его пользователю, а второй параметр *http.Request является указателем на структуру, которая содержит информацию о текущем запросе (вроде HTTP-методов POST, GET, DELETE… и URL текущего запроса). В будущих уроках мы поговорим об этом подробнее.
Когда вы запустите этот код, он должен запустить веб-сервер на порт 4000 вашего компьютера. Каждый раз при отправки пользователем нового HTTP-запроса этот запрос получает веб-сервер, который проверяет URL и отправляет данный запрос соответствующему обработчику.
Сетевые интерфейсы в Golang
В других проектах или документации по Go иногда можно увидеть сетевые адреса, записанные с использованием именованных портов, таких как «:http» или «:http-alt» вместо числа. Если используется именованный порт, Go попытается найти соответствующий номер порта в файле /etc/services при запуске веб-сервера. Если совпадение не будет найдено, Go вернет ошибку.
Запуск веб-приложения в Golang
Команда go run принимает:
На данный момент для нашего приложения все три команды эквивалентны:
Разработка веб-приложения на Golang
В этой статье я рассмотрю разработку веб-приложения на Go. Материал не содержит принципиально новых знаний и рассчитан скорее для таких же новоиспеченных исследователей языка как и я. Хотя, надеюсь, какие-то свежие идеи вы все-таки для себя найдете.
У некоторых читателей могут возникнуть вопросы о «велосипедостроении» — это всё плоды любопытства и живого интереса при ознакомлении с языком Golang.
Администрирование системы и разработка проекта
Лишь мельком обозначу этот пункт, чтобы по кусочкам иметь представление о единой системе. В конечном счете CI-сервер собирает проект из git-репозитория и формирует полноценный rpm-пакет для нужной архитектуры, который устанавливается в систему как systemd-сервис.
Установка, обновление и откат веб-приложения целиком ложатся на пакетный менеджер linux-системы (yum/dnf/rpm), в результате чего эта иногда нетривиальная задача становиться простой и надежной.
Основная логика
Для некоторых задач мы будем пользоваться готовым тулкитом Gorilla toolkit и на его основе, по сути, сделаем свой несколько расширенный тулкит.
Инициализация приложения
Приложение имеет объекты, которые изменяются лишь однажды при старте — это структуры конфигурации, роутеров, объекты доступа к базе данных и шаблонам. Для консолидации и удобного их применения, создадим структуру Application:
Объект Application в приложении конечно же должен быть один:
Таким образом, использование нашего Application будет достаточно простым:
httpHandler с контекстом *Context
Самое интересное здесь именно установление роутеров:
Дело в том, что в Router из тулкита Gorilla ровно как и в стандартной библиотеке «net/http» работа обработчика (контроллера) сводится к функции типа func(http.ResponseWriter, *http.Request). Нам же интересен другой вид контроллера, чтобы не дублировать код из контроллера в контроллер тривиальными операциями:
где *Context — удобный инструмент работы с куками, сессией и другими контекстно-зависимыми структурами. Если говорить более детально, то нас интересует не только контекст реквеста в контроллере, но и доступ к БД, к конфигурации, т.е. и к объекту Application. Для этого вводим функцию обертку obs(handler Controller) func(http.ResponseWriter, *http.Request), которая на вход получает нужный нам вид контроллера — интерфейс Controller, а возвращает нужный для r.HandleFunc() вид функции и при этом выполняет все надстроечные действия перед выполнением контроллера — создание *ContextApplication объекта.
Создание контроллера
Теперь все готово для создание контроллера:
Процесс создания нового контроллера заключается в переписывании методов встроенного app.HTTPController объекта (GET, POST и т.п.). Если не переписать метод, то вызовется встроенный, который возвращает клиенту «Method not allowed» (это поведение можно изменить на любое другое).
Контекст
Context по сути состоит из набора методов для упрощения работы с контекстно-зависимыми переменными. Не буду писать реализацию, вкратце перечислю некоторые методы, чтобы было ясно о чем идет речь:
Шаблонизатор
В составе стандартной библиотеки есть замечательный пакет «html/template». Его и будем использовать, немного расширив его функционал.
AbstractPage является контейнером входных данных для использования их в template’ах. Приведу пример:
Разработка веб-серверов на Golang — от простого к сложному
Пять лет назад я начал разрабатывать Gophish, это дало возможность изучить Golang. Я понял, что Go — мощный язык, возможности которого дополняются множеством библиотек. Go универсален: в частности, с его помощью можно без проблем разрабатывать серверные приложения.
Эта статья посвящена написанию сервера на Go. Начнем с простых вещей, вроде «Hello world!», а закончим приложением с такими возможностями:
— Использование Let’s Encrypt для HTTPS.
— Работа в качестве API-маршрутизатора.
— Работа с middleware.
— Обработка статических файлов.
— Корректное завершение работы.
Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».
Hello, world!
Создать веб-сервер на Go можно очень быстро. Вот пример использования обработчика, который возвращает обещанный выше «Hello, world!».
После этого, если запустить приложение и открыть страницу localhost, то вы сразу увидите текст «Hello, world!» (конечно, если все работает правильно).
Далее мы будем неоднократно использовать обработчик, но сначала давайте поймем, как все устроено.
net/http
HTTP-обработчики
Когда мы получаем запрос, обработчик анализирует его и формирует ответ. Обработчики в Go реализованы следующим образом:
В первом примере используется вспомогательная функция http.HandleFunc. Она оборачивает другую функцию, которая, в свою очередь, принимает http.ResponseWriter и http.Request в ServeHTTP.
Другими словами, обработчики в Golang представлены единым интерфейсом, что дает множество возможностей для программиста. Так, например, middleware реализовано при помощи обработчика, где ServeHTTP сначала что-то делает, а затем вызывает метод ServeHTTP другого обработчика.
Как и говорилось выше, обработчики просто формируют ответы на запросы. Но какой именно обработчик нужно использовать в конкретный момент времени?
Маршрутизация запросов
Для того, чтобы сделать правильный выбор, воспользуйтесь HTTP-мультиплексором. В ряде библиотек его называют muxer или router, но все это одно и то же. Функция мультиплексора заключается в анализе пути запроса и выборе соответствующего обработчика.
Если же нужна поддержка сложной маршрутизации, тогда лучше воспользоваться сторонними библиотеками. Одни из наиболее продвинутых — gorilla/mux и go-chi/chi, эти библиотеки дают возможность реализовать промежуточную обработку без особых проблем. С их помощью можно настроить wildcard-маршрутизацию и выполнить ряд других задач. Их плюс — совместимость со стандартными HTTP-обработчиками. В результате вы можете написать простой код с возможностью его модификации в будущем.
Работа со сложными фреймворками в обычной ситуации потребует не совсем стандартных решений, а это значительно усложняет использование дефолтных обработчиков. Для создания подавляющего большинства приложений хватит комбинации библиотеки по умолчанию и несложного маршрутизатора.
Обработка запросов
Кроме того, нам необходим компонент, который будет «слушать» входящие соединения и перенаправлять все запросы правильному обработчику. С этой задачей без труда справится http.Server.
Ниже показано, что сервер отвечает за все задачи, которые имеют отношение к обработке соединений. Это, например, работа по протоколу TLS. Для реализации вызова http.ListenAndServer используется стандартный HTTP-сервер.
Теперь давайте рассмотрим более сложные примеры.
Добавление Let’s Encrypt
По умолчанию наше приложение работает по HTTP-протоколу, однако рекомендуется использовать протокол HTTPS. В Go это можно сделать без проблем. Если вы получили сертификат и закрытый ключ, тогда достаточно прописать ListenAndServeTLS с указанием правильных файлов сертификата и ключа.
Всегда можно сделать лучше.
Самый простой способ его настроить — воспользоваться методом autocert.NewListener в комбинации с http.Serve. Метод позволяет получать и обновлять TLS-сертификаты, в то время как HTTP-сервер обрабатывает запросы:
Если мы откроем в браузере example.com, то получим HTTPS-ответ «Hello, world!».
Если нужна более тщательная настройка, то стоит воспользоваться менеджером autocert.Manager. Затем создаем собственный инстанс http.Server (до настоящего момента мы использовали его по умолчанию) и добавить менеджер в сервер TLSConfig:
Это простой способ реализации полной поддержки HTTPS с автоматическим обновлением сертификата.
Добавление нестандартных маршрутов
Дефолтный маршрутизатор, включенный в стандартную библиотеку, хорош, но он очень простой. В большинстве приложений нужна более сложная маршрутизация, включая вложенные и wildcard-маршруты или же процедуру установки шаблонов и параметров путей.
В этом случае стоит использовать пакеты gorilla/mux и go-chi/chi. С последним мы и научимся работать — ниже показан пример.
Дано — файл api/v1/api.go, содержащий маршруты для нашего API:
Устанавливаем для маршрутов префикс api/vq в основном файле.
We can then mount this to our main router under the api/v1/ prefix back in our main application:
Простота работы со сложными маршрутами в Go делает возможным упростить структуризацию с обслуживанием больших комплексных приложений.
Работа с middleware
В случае промежуточной обработки используется оборачивание одного HTTP-обработчика другим, что делает возможным быстро проводить аутентификацию, сжатие, журналирование и некоторые другие функции.
В качестве примера рассмотрим интерфейс http.Handler, с его помощью напишем обработчик с аутентификацией пользователей сервиса.
Есть сторонние маршрутизаторы, например, chi, которые позволяют расширить функциональность промежуточной обработки.
Работа со статическими файлами
В стандартную библиотеку Go входят возможности для работы со статическим контентом, включая изображения, а также файлы JavaScript и CSS. Доступ к ним можно получить через функцию http.FileServer. Она возвращает обработчик, который раздает файлы из определенной директории.
Корректное завершение работы
В Go есть и такая функция, как корректное завершение работы HTTP-сервера. Это можно сделать при помощи метода Shutdown(). Сервер запускается в горутине, а далее канал прослушивается для приема сигнала прерывания. Как только сигнал получен, сервер отключается, но не сразу, а через несколько секунд.
Веб-разработка на Go
Установка Go
Итак, первое, что необходимо для работы с Go — Linux, FreeBSD (OS X), хотя MinGW под Windows тоже сойдёт.
Go необходимо установить, для этого нужно выполнить примерно следующее (инструкции приведены для систем с dpkg):
Если всё хорошо, можно добавить в
При повторном входе в шелл и вызове компилятора (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 и выведется на экран.
Для сборки и запуска программы необходимо выполнить следующее:
Библиотека http
Самый простой веб-сервер на 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 для выдачи страниц
Импортируем библиотеку 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
Библиотека template входит в стандартную библиотеку Go. Мы можем использовать шаблоны для хранения разметки html вне кода, чтобы можно было менять разметку без перекомпиляции.
Сперва, импортируем template:
Создадим шаблон формы в файле edit.html, со следующим содержимым:
Изменим editHandler таким образом, чтобы использовать шаблон:
Метод template.ParseFile прочтёт файл edit.html и выдаст значение типа *template.Template.
Метод t.Execute заменяет все вхождения и на значения p.title и p.body, и выводит полученный html в переменную типа http.ResponseWriter.
Заметьте, в шаблоне встречалась конструкция . Она означает, что параметр будет отформатирован для вывода в 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 в коде пришлось разбить, дабы не выводить из себя хабрапарсер.