[Из песочницы] Разработка веб-приложения на Golang

В этой статье я рассмотрю разработку веб-приложения на Go. Материал не содержит принципиально новых знаний и рассчитан скорее для таких же новоиспеченных исследователей языка как и я. Хотя, надеюсь, какие-то свежие идеи вы все-таки для себя найдете.У некоторых читателей могут возникнуть вопросы о «велосипедостроении» — это всё плоды любопытства и живого интереса при ознакомлении с языком Golang.

Администрирование системы и разработка проектаЛишь мельком обозначу этот пункт, чтобы по кусочкам иметь представление о единой системе. В конечном счете CI-сервер собирает проект из git-репозитория и формирует полноценный rpm-пакет для нужной архитектуры, который устанавливается в систему как systemd-сервис. Description=Description After=network.target Requires=mysqld.service

[Service] Type=simple User=nginx Group=nginx

WorkingDirectory=/usr/share/project_name

StandardOutput=journal StandardError=journal

ExecStart=/usr/share/project_name/project_name Restart=always

[Install] WantedBy=multi-user.target Системный менеджер systemd занимается: Установлением зависимостей запуска веб-сервиса (как в вышеуказанном примере от mysqld); Respawn-ом на случай падения приложения; Благодаря опциям StandardOutput и StandardError, логированием службы. Чтобы из приложения писать в системный лог, достаточно вызвать: log.Println («Server is preparing to start») Впереди устанавливается http-сервер для отдачи статики, например, nginx.Установка, обновление и откат веб-приложения целиком ложатся на пакетный менеджер linux-системы (yum/dnf/rpm), в результате чего эта иногда нетривиальная задача становиться простой и надежной.

Основная логика Для некоторых задач мы будем пользоваться готовым тулкитом Gorilla toolkit и на его основе, по сути, сделаем свой несколько расширенный тулкит.Инициализация приложения Приложение имеет объекты, которые изменяются лишь однажды при старте — это структуры конфигурации, роутеров, объекты доступа к базе данных и шаблонам. Для консолидации и удобного их применения, создадим структуру Application: type MapRoutes map[string]Controller

type Application struct { Doc AbstractPage Config Config DB SQL

routes MapRoutes } Методы Application // Routes устанавливает обработчики запросов в соответствии с URL’ами func (app *Application) Routes (r MapRoutes) { app.routes = r }

func (app *Application) Run () { r:= mux.NewRouter () r.StrictSlash (true)

for url, ctrl:= range app.routes { r.HandleFunc (url, obs (ctrl)) }

http.Handle (»/», r) listen:= fmt.Sprintf (»%s:%d», app.Config.Net.Listen_host, app.Config.Net.Listen_port)

log.Println («Server is started on», listen) if err:= http.ListenAndServe (listen, nil); err!= nil { log.Println (err) } } Объект Application в приложении конечно же должен быть один: var appInstance *Application

// GetApplication возвращает экземпляр Application func GetApplication () *Application { if appInstance == nil { appInstance = new (Application)

// Init code appInstance.Config = loadConfig («config.ini») appInstance.Doc = make (AbstractPage) appInstance.routes = make (MapRoutes) // … }

return appInstance } Таким образом, использование нашего Application будет достаточно простым: main.go package main

import ( «interfaces/app» «interfaces/handlers» «log» )

func init () { log.SetFlags (log.LstdFlags | log.Lshortfile) }

func main () { log.Println («Server is preparing to start») Application:= app.GetApplication ()

if Application.Config.Site.Disabled { log.Println («Site is disabled») Application.Routes (app.MapRoutes{»/»: handlers.HandleDisabled{}}) } else { Application.Routes (app.MapRoutes{ »/»: handlers.HandleHome{}, »/v1/ajax/»: handlers.HandleAjax{}, // другие контроллеры »/{url:.*}»: handlers.Handle404{}, }) }

Application.Run () log.Println («Exit») } httpHandler с контекстом *Context Самое интересное здесь именно установление роутеров: for url, ctrl:= range app.routes { r.HandleFunc (url, obs (ctrl)) } Дело в том, что в Router из тулкита Gorilla ровно как и в стандартной библиотеке «net/http» работа обработчика (контроллера) сводится к функции типа func (http.ResponseWriter, *http.Request). Нам же интересен другой вид контроллера, чтобы не дублировать код из контроллера в контроллер тривиальными операциями: func ProductHandler (ctx *Context) { // … } где *Context — удобный инструмент работы с куками, сессией и другими контекстно-зависимыми структурами. Если говорить более детально, то нас интересует не только контекст реквеста в контроллере, но и доступ к БД, к конфигурации, т.е. и к объекту Application. Для этого вводим функцию обертку obs (handler Controller) func (http.ResponseWriter, *http.Request), которая на вход получает нужный нам вид контроллера — интерфейс Controller, а возвращает нужный для r.HandleFunc () вид функции и при этом выполняет все надстроечные действия перед выполнением контроллера — создание *ContextApplication объекта.Функция obs (), Controller и HTTPController type Controller interface {

GET (app *ContextApplication) POST (app *ContextApplication) PUT (app *ContextApplication) DELETE (app *ContextApplication) PATCH (app *ContextApplication) OPTIONS (app *ContextApplication) HEAD (app *ContextApplication) TRACE (app *ContextApplication) CONNECT (app *ContextApplication) }

// obs инициализирует контекст для заданного клиента и вызывает контроллер func obs (handler Controller) func (http.ResponseWriter, *http.Request) { return func (w http.ResponseWriter, req *http.Request) {

ctx:= context.New (w, req) app:= GetApplication () doc:= app.Doc.Clone (») doc[«Ctx»] = ctx doc[«User»] = ctx.User ()

contextApp:= &ContextApplication{ctx, doc, app.Config, app.DB}

switch ctx.Input.Method () { case «GET»: handler.GET (contextApp); case «POST»: handler.POST (contextApp); case «PUT»: handler.PUT (contextApp); case «DELETE»: handler.DELETE (contextApp); case «PATCH»: handler.PATCH (contextApp); case «OPTIONS»: handler.OPTIONS (contextApp); case «HEAD»: handler.HEAD (contextApp); case «TRACE»: handler.TRACE (contextApp); case «CONNECT»: handler.CONNECT (contextApp);

default: http.Error (ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) } } }

// HTTPController объект для встраивания в контроллеры, содержащие стандартные методы для контроллера // Задача контроллеров переписать необходимые методы. type HTTPController struct {}

func (h HTTPController) GET (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) POST (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) PUT (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) DELETE (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) PATCH (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) OPTIONS (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) HEAD (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) TRACE (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) }

func (h HTTPController) CONNECT (app *ContextApplication) { http.Error (app.Ctx.Response (), «Method not allowed», http.StatusMethodNotAllowed) } *ContextApplication type ContextApplication struct { Ctx *context.Context Doc AbstractPage Config Config DB SQL } Создание контроллера Теперь все готово для создание контроллера: HandleCustom import ( «interfaces/app» )

type HandleCustom struct { app.HTTPController }

func (h HandleCustom) GET (app *app.ContextApplication) { app.Ctx.SendHTML («html data here») }

func (h HandleCustom) POST (app *app.ContextApplication) { // and so on… } Процесс создания нового контроллера заключается в переписывании методов встроенного app.HTTPController объекта (GET, POST и т.п.). Если не переписать метод, то вызовется встроенный, который возвращает клиенту «Method not allowed» (это поведение можно изменить на любое другое).Контекст Context по сути состоит из набора методов для упрощения работы с контекстно-зависимыми переменными. Не буду писать реализацию, вкратце перечислю некоторые методы, чтобы было ясно о чем идет речь: func (c *Context) NotFound () // NotFound sends page with 404 http code from template tpls/404.tpl func (c *Context) Redirect (url string) // Redirect sends http redirect with 301 code func (c *Context) Redirect303(url string) // Redirect303 sends http redirect with 303 code func (c *Context) SendJSON (data string) int // SendJSON sends json-content (data) func (c *Context) SendXML (data string) // SendXML sends xml-content (data) func (c *Context) GetCookie (key string) string // GetCookie return cookie from request by a given key. func (c *Context) SetCookie (name string, value string, others …interface{}) // SetCookie set cookie for response. func (c *Context) CheckXsrfToken () bool // CheckXsrfToken проверяет token func (c *Context) User () User // User возвращает текущего пользователя func (c *Context) Session (name string) (*Session, error) // Session открывает сессию func (s *Session) Clear () // Clear очищает открытую сессию

// и т.д. Шаблонизатор В составе стандартной библиотеки есть замечательный пакет «html/template». Его и будем использовать, немного расширив его функционал. // loadTemplate load template from tpls/%s.tpl func loadTemplate (Name string) *html.Template { funcMap:= html.FuncMap{ «html»: func (val string) html.HTML { return html.HTML (val) }, «typo»: func (val string) string { return typo.Typo (val) }, «mod»: func (args …interface{}) interface{} { if len (args) == 0 { return » }

name:= args[0].(string) ctx:= new (context.Context)

if len (args) > 1 { ctx = args[1].(*context.Context) }

modules:= reflect.ValueOf (modules.Get ()) mod:= modules.MethodByName (name)

if (mod == reflect.Value{}) { return » }

inputs:= make ([]reflect.Value, 0) inputs = append (inputs, reflect.ValueOf (ctx))

ret:= mod.Call (inputs) return ret[0].Interface () }, }

return html.Must (html.New (»*»).Funcs (funcMap).Delims (»{{%»,»%}}»).ParseFiles («tpls/» + Name + ».tpl»)) } Для совместимости с AngularJS меняем разделители с »{{ }}» на »{{% %}}», хотя, признаюсь, не совсем удобно.Более подробно о 3-х вышеуказанных pipeline-функций: html — меняет тип входного параметра на HTML, чтобы шаблон не экранировал HTML-строки. Иногда бывает полезно. Пример использования в шаблоне:

{{% .htmlString | html %}}
typo — обработка текста по некоторым типографическим правилам. Пример использования в шаблоне:

{{% .title | typo %}}

mod — запуск модулей прямо из тела шаблона. Пример использования:
{{% mod «InformMenu» %}}
type AbstractPage map[string]interface{} AbstractPage является контейнером входных данных для использования их в template’ах. Приведу пример: Заполнение значений в коде func (h HandleCustom) GET (app *app.ContextApplication) { doc:= app.Doc.Clone («custom») // Создается новый AbstractPage, который будет использовать custom.tpl doc[«V1»] = «V1» doc[«V2»] = 555

result:= doc.Compile () app.Ctx.SendHTML (result) } custom.tpl {{%define »*»%}}

  • {{% .V1%}}
  • {{% .V2%}}
{{%end%}} AbstractPage имеет 2 метода: Метод Clone () // Clone возвращает новый экземпляр AbstractPage c наследованными полями и значениями func (page AbstractPage) Clone (tplName string) AbstractPage { doc:= make (AbstractPage) for k, v:= range page { doc[k] = v }

doc[»__tpl»] = tplName return doc } Создает новый контейнер AbstractPage, копируя все значения. Смысл этой операции заключается в наследовании значений с вышестоящих уровней AbstractPage. Метод Compile () // Compile return page formatted with template from tpls/%d.tpl func (page AbstractPage) Compile () string { var data bytes.Buffer

for k, v:= range page { switch val:= v.(type) { case AbstractPage: { page[k] = html.HTML (val.Compile ()) } case func ()string: { page[k] = val () } } }

// Директива загрузки модулей динамичная (ctx записан в doc[«Ctx»]) getTpl (page[»__tpl»].(string)).Execute (&data, page)

return data.String () } Выполняет прогон шаблона и формирует результирующий HTML-код. Резюме На мой взгляд, получилось гибко и довольно просто. Остальная разработка связана с реализацией конкретных контроллеров и модулей, которые по своей природе независимы друг от друга.Хотелось бы отметить, что Go не оставил меня равнодушным, также как и многих.

Ссылки 1. github.com/dblokhin/typo — golang package для обработки текста по некоторым типографическим правилам.

© Habrahabr.ru