Розробка веб-додатки на Golang

У цій статті я розгляну розробку веб-додатки на Go. Матеріал не містить принципово нових знань і розрахований швидше для таких новоспечених дослідників мови як і я. Хоча, сподіваюся, якісь свіжі ідеї ви все-таки для себе знайдете.

У деяких читачів може виникнути питання про «велосипедостроении» — це все плоди цікавості і живого інтересу при ознайомленні з мовою Golang.

Адміністрування системи і розробка проекту

Лише мигцем позначу цей пункт, щоб по шматочках мати уявлення про єдину систему. У кінцевому рахунку CI-сервер збирає проект з git-репозиторію і формує повноцінний rpm-пакет для потрібної архітектури, який встановлюється в систему як systemd-сервіс.

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 займається:
  1. Встановленням залежностей запуску веб-сервісу (як у вищевказаному прикладі від mysqld);
  2. Respawn-му на випадок падіння програми;
  3. Завдяки опцій 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
SQL DB

routes MapRoutes
}

Методи Application

// Routes встановлює обробник запитів відповідно до ДО амі
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
SQL DB
}


Створення контролера
Тепер все готово для створення контролера:

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(string key) string // GetCookie return cookie request from by a given key.
func (c *Context) SetCookie(string name, 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(string name) (*Session, error) // Session відкриває сесію
func (s *Session) Clear() // Clear очищає відкриту сесію

// і т. д.


Шаблонизатор
У складі стандартної бібліотеки є чудовий пакет «html/template». Його і будемо використовувати, трохи розширивши його функціонал.


// loadTemplate load from template tpls/%s.tpl
func loadTemplate(string Name) *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-функцій:
  1. html — змінює тип вхідного параметра на HTML, щоб шаблон не екранований HTML-рядка. Іноді буває корисно. Приклад використання шаблону:
    <div>{{% .htmlString | html %}}</div>
  2. typo — обробка тексту за деякими типографическим правилами. Приклад використання шаблону:
    <h1>{{% .title | typo %}}</h1>
  3. mod — запуск модулів прямо з тіла шаблону. Приклад використання:
    <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"] = "1"
doc["V2"] = 555

result := doc.Compile()
app.Ctx.SendHTML(result)
}


custom.tpl

{{%define ".*"%}}
<ul>
<li>{{% .V1 %}}</li>
<li>{{% .V2 %}}</li>
</ul>
{{%end%}}


AbstractPage має 2 методу:
  1. Метод 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.
  2. Метод Compile()
    
    // Compile return page formatted with from template 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 для обробки тексту за деякими типографическим правилами.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.