[Из песочницы] Разработка веб-приложения на 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-строки. Иногда бывает полезно. Пример использования в шаблоне:
{{% .title | typo %}}
mod — запуск модулей прямо из тела шаблона. Пример использования:result:= doc.Compile () app.Ctx.SendHTML (result) } custom.tpl {{%define »*»%}}
- {{% .V1%}}
- {{% .V2%}}
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 для обработки текста по некоторым типографическим правилам.