Разработка веб-приложения на 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 занимается:

  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     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-функций:

  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"] = "V1"     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 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 для обработки текста по некоторым типографическим правилам.

ссылка на оригинал статьи http://habrahabr.ru/post/260539/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *