- PVSM.RU - https://www.pvsm.ru -
В этой статье я рассмотрю разработку веб-приложения на Go. Материал не содержит принципиально новых знаний и рассчитан скорее для таких же новоиспеченных исследователей языка как и я. Хотя, надеюсь, какие-то свежие идеи вы все-таки для себя найдете.
У некоторых читателей могут возникнуть вопросы о «велосипедостроении» — это всё плоды любопытства и живого интереса при ознакомлении с языком Golang.
Лишь мельком обозначу этот пункт, чтобы по кусочкам иметь представление о единой системе. В конечном счете CI-сервер собирает проект из git-репозитория и формирует полноценный rpm-пакет для нужной архитектуры, который устанавливается в систему как systemd-сервис.
[Unit]
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 занимается:
log.Println("Server is preparing to start")
Впереди устанавливается http-сервер для отдачи статики, например, nginx.
Установка, обновление и откат веб-приложения целиком ложатся на пакетный менеджер linux-системы (yum/dnf/rpm), в результате чего эта иногда нетривиальная задача становиться простой и надежной.
Для некоторых задач мы будем пользоваться готовым тулкитом Gorilla toolkit [1] и на его основе, по сути, сделаем свой несколько расширенный тулкит.
Приложение имеет объекты, которые изменяются лишь однажды при старте — это структуры конфигурации, роутеров, объекты доступа к базе данных и шаблонам. Для консолидации и удобного их применения, создадим структуру Application:
type MapRoutes map[string]Controller
type Application struct {
Doc AbstractPage
Config Config
DB SQL
routes MapRoutes
}
// 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 будет достаточно простым:
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")
}
Самое интересное здесь именно установление роутеров:
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 объекта.
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)
}
type ContextApplication struct {
Ctx *context.Context
Doc AbstractPage
Config Config
DB SQL
}
Теперь все готово для создание контроллера:
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-функций:
<div>{{% .htmlString | html %}}</div>
<h1>{{% .title | typo %}}</h1>
<div>{{% mod "InformMenu" %}}</div>
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)
}
{{%define "*"%}}
<ul>
<li>{{% .V1 %}}</li>
<li>{{% .V2 %}}</li>
</ul>
{{%end%}}
AbstractPage имеет 2 метода:
// 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 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 [2] — golang package для обработки текста по некоторым типографическим правилам.
Автор: dblokhin
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/golang/92355
Ссылки в тексте:
[1] Gorilla toolkit: http://www.gorillatoolkit.org/
[2] github.com/dblokhin/typo: https://github.com/dblokhin/typo
[3] Источник: http://habrahabr.ru/post/260539/
Нажмите здесь для печати.