📜 ⬆️ ⬇️

Web application development on Golang

In this article I will discuss the development of a web application on Go. The material does not contain fundamentally new knowledge and is calculated rather for the same newly-fledged researchers of the language as I am. Although, I hope, you will find some fresh ideas for yourself.

Some readers may have questions about “cycling” - this is all the fruits of curiosity and keen interest in learning the language of Golang.

System administration and project development


I will only briefly mark this point in order to have an idea of ​​a single system in pieces. Ultimately, the CI server collects the project from the git repository and forms a complete rpm package for the desired architecture, which is installed into the system as a systemd service.

[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 System Manager deals with:
  1. Establishing dependencies of web service launch (as in the above example from mysqld);
  2. Respawn in case the application crashes;
  3. Thanks to the StandardOutput and StandardError options, logging the service. To write to the system log from the application, just call:
     log.Println("Server is preparing to start") 

An http server is installed ahead of us to render statics, for example, nginx.
')
Installing, updating, and rolling back a web application falls entirely on the package manager of the linux system (yum / dnf / rpm), with the result that this sometimes non-trivial task becomes simple and reliable.

Basic logic


For some tasks, we will use the ready-made Gorilla toolkit toolkit and, based on it, in essence, we will make our somewhat advanced toolkit.

Application Initialization

The application has objects that change only once at startup - these are configuration structures, routers, database access objects and templates. To consolidate and conveniently apply them, let's create an Application structure:

 type MapRoutes map[string]Controller type Application struct { Doc AbstractPage Config Config DB SQL routes MapRoutes } 

Application Methods
 // 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) } } 


The Application object in the application should of course be one:

 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 } 

Thus, using our Application will be quite simple:

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 with context * Context

The most interesting thing here is the establishment of routers:

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

The fact is that in the Router from the Gorilla toolkit exactly as in the standard net / http library, the work of the handler (controller) is reduced to a function of the func type (http.ResponseWriter, * http.Request) . We are also interested in another type of controller, so as not to duplicate the code from the controller to the controller with trivial operations:

 func ProductHandler(ctx *Context) { // ... } 

where * Context is a handy tool for working with cookies, session and other context-sensitive structures. In more detail, we are interested not only in the context of the request in the controller, but also in access to the database, to the configuration, i.e. and to the Application object. To do this, we introduce the obs (handler Controller) wrapper function func (http.ResponseWriter, * http.Request) , which receives the controller type we need — the Controller interface — and returns the function type needed for r.HandleFunc () and at the same time performs all Add-on actions before the controller is executed - creating * ContextApplication object.

Function obs (), Controller and 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 } 


Creating a controller

Now everything is ready to create a controller:

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... } 


The process of creating a new controller is to rewrite the methods of the embedded app.HTTPController object (GET, POST, etc.). If you do not rewrite the method, the built-in method will be called up, which returns to the client “Method not allowed” (this behavior can be changed to any other).

Context

Context essentially consists of a set of methods to simplify working with context-sensitive variables. I will not write an implementation, briefly enumerate some methods so that it is clear what is at stake:

 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    //  .. 


Template engine

As part of the standard library there is a wonderful package "html / template". And we will use it, slightly expanding its functionality.

 // 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")) } 

For compatibility with AngularJS, we change the delimiters from "{{}}" to "{{%%}}", although, I confess, it is not very convenient.
In more detail about 3 above pipeline-functions:
  1. html - changes the type of the input parameter to HTML, so that the template does not screen HTML lines. Sometimes it is useful. Example of use in the template:
     <div>{{% .htmlString | html %}}</div> 
  2. typo - text processing according to some typographical rules. Example of use in the template:
     <h1>{{% .title | typo %}}</h1> 
  3. mod - launch modules directly from the template body. Usage example:
     <div>{{% mod "InformMenu" %}}</div> 


 type AbstractPage map[string]interface{} 

AbstractPage is a container of input data for use in templates. I will give an example:

Filling values ​​in the code
 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 has 2 methods:
  1. Clone () method
     // 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 } 


    Creates a new AbstractPage container by copying all the values. The meaning of this operation is the inheritance of values ​​from higher levels of AbstractPage.
  2. Compile () method
     // 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() } 


    Runs the template and generates the resulting HTML code.


Summary


In my opinion, it turned out flexible and quite simple. The rest of the development is associated with the implementation of specific controllers and modules that are independent in nature.

I would like to note that Go did not leave me indifferent, as well as many.

Links


1. github.com/dblokhin/typo - golang package for processing text according to some typographical rules.

Source: https://habr.com/ru/post/260539/


All Articles