So, our application will have two main parts: client and server. (What is the year now?). The server part will be on Go, and the client part will be on JS. Let's first talk about the server side.
The server part of our application will be responsible for the initial maintenance of everything necessary for JavaScript and everything else, such as static files and data in JSON format. This is all, only two functionalities: (1) static and (2) JSON.
It is worth noting that static maintenance is optional: statics can be served in a CDN, for example. But the important thing is that this is not a problem for our Go application - unlike the Python / Ruby application, it can work on a par with Ngnix and Apache serving static. Delegating the distribution of static files to some other application is not particularly required to ease the load, although it makes sense in some situations.
For simplicity, let's imagine that we are creating an application that serves a list of people (only first and last names), stored in database tables, and that's it. The code is here - https://github.com/grisha/gowebapp .
As my experience shows, separating the functionality between packages at an early stage is a good idea in Go. Even if it is not entirely clear how the final version will be structured, it’s best to keep everything in its unfolded state whenever possible.
For a web application, in my opinion, this layout makes sense:
# github.com/user/foo foo/ # package main | +--daemon/ # package daemon | +--model/ # package model | +--ui/ # package ui | +--db/ # package db | +--assets/ # JS
main
packageAt the top level, we have the main
package, and its code is in the main.go
file. The main advantage is that in this situation go get github.com/user/foo
is the only command required to install the entire application in $GOPATH/bin
.
The main
package should be as minimal as possible. The only code that is here is the analysis of the arguments of the command. If the application had a configuration file, I would place the parsing and verification of this file into another package, which would most likely call the config
. After this, main
should transfer control to the daemon
package.
Here is the basis of main.go
:
package main import ( "github.com/user/foo/daemon" ) var assetsPath string func processFlags() *daemon.Config { cfg := &daemon.Config{} flag.StringVar(&cfg.ListenSpec, "listen", "localhost:3000", "HTTP listen spec") flag.StringVar(&cfg.Db.ConnectString, "db-connect", "host=/var/run/postgresql dbname=gowebapp sslmode=disable", "DB Connect String") flag.StringVar(&assetsPath, "assets-path", "assets", "Path to assets dir") flag.Parse() return cfg } func setupHttpAssets(cfg *daemon.Config) { log.Printf("Assets served from %q.", assetsPath) cfg.UI.Assets = http.Dir(assetsPath) } func main() { cfg := processFlags() setupHttpAssets(cfg) if err := daemon.Run(cfg); err != nil { log.Printf("Error in main(): %v", err) } }
The following code takes three parameters: -listen
, -db-connect
and -assets-path
, nothing special.
In the cfg := &daemon.Config{}
we create a daemon.Config
object. Its main purpose is to present the configuration in a structured and understandable format. Each of our packages defines its own Config
type, which describes the parameters it needs, and which may include settings for other packages. We see an example of this in the processFlags()
above: flag.StringVar(&cfg.Db.ConnectString, ...
Here db.Config
included in daemon.Config
. In my opinion, this is a very useful technique. Using structures also leaves the possibility of serializing settings in the form of JSON, TOML or something else.
http.Dir(assetsPath)
in setupHttpAssets
is a preparation for how we will serve statics in the ui
package. This is done in such a way as to leave the possibility for another implementation of cfg.UI.Assets
(which is the http.FileSystem
interface), for example, to give this content from RAM. I will talk about this in more detail later, in a separate post.
In the end, main
calls daemon.Run(cfg)
, which actually starts our application and blocks until the end of the work.
daemon
The daemon
package contains everything related to running the process. This includes, for example, which port will be tapped, a user log will be defined here, as well as everything related to a polite restart, etc.
Since the task of the daemon
package is to initialize the connection to the database, it needs to import the db
package. He is also responsible for listening to the TCP port and launching the user interface for this listener, so he needs to import the ui
package, and since the ui
package needs to have access to the data provided by the model
package, it also needs to import the model
package.
The daemon
module skeleton looks like this:
package daemon import ( "log" "net" "os" "os/signal" "syscall" "github.com/grisha/gowebapp/db" "github.com/grisha/gowebapp/model" "github.com/grisha/gowebapp/ui" ) type Config struct { ListenSpec string Db db.Config UI ui.Config } func Run(cfg *Config) error { log.Printf("Starting, HTTP on: %s\n", cfg.ListenSpec) db, err := db.InitDb(cfg.Db) if err != nil { log.Printf("Error initializing database: %v\n", err) return err } m := model.New(db) l, err := net.Listen("tcp", cfg.ListenSpec) if err != nil { log.Printf("Error creating listener: %v\n", err) return err } ui.Start(cfg.UI, m, l) waitForSignal() return nil } func waitForSignal() { ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) s := <-ch log.Printf("Got signal: %v, exiting.", s) }
Note that Config
includes db.Config
and ui.Config
, as I mentioned.
All action takes place in Run(*Config)
. We initialize the connection to the database, create an instance of model.Model
and run ui
, passing it the settings, pointers to the model and the listener.
model
The purpose of the model
is to separate how the data is stored in the database from the ui
, as well as to provide the business logic that the application can have. This is the brain of your application, if you like.
The model
package must define a structure ( Model
looks like a suitable name), and a pointer to an instance of this structure must be passed to all functions and methods ui
. In our application, there should be only one such instance - for additional confidence, you can implement this programmatically using a singleton, but I do not think that this is so necessary.
Alternatively, you can do without the Model
structure and simply use the model
package itself. I do not like this approach, however this is an option.
The model must also define structures for the data entities with which we are dealing. In our example, this will be the Person
structure. Its members must be exported (named with a capital letter), because other packages will access them. If you use sqlx , here you must specify the tags that bind the elements of the structure to the names of the columns in the database, for example, db:"first_name"
.
Our Person
Type:
type Person struct { Id int64 First, Last string }
Here we do not need tags, because the names of the columns correspond to the names of the elements of the structure, and sqlx
takes care of the register so that Last
corresponds to the column with the name last
.
model
package should NOT import db
Somewhat counterintuitively, model
should not import db
. But it should not because the db
package needs to import the model
, and cyclical imports are prohibited in Go. This is the case when interfaces come in handy. model
must specify an interface that db
should satisfy. So far we only know that we need a list of people, so we can start with this definition:
type db interface { SelectPeople() ([]*Person, error) }
Our application does not do much, but we know that it lists people, so our model, most likely, should have the People() ([]*Person, error)
method People() ([]*Person, error)
:
func (m *Model) People() ([]*Person, error) { return m.SelectPeople() }
For everything to be neat, it is better to place the code in different files, for example, the Person
structure must be defined in person.go
, etc. But for readability, here is the single-file version of our model
package:
package model type db interface { SelectPeople() ([]*Person, error) } type Model struct { db } func New(db db) *Model { return &Model{ db: db, } } func (m *Model) People() ([]*Person, error) { return m.SelectPeople() } type Person struct { Id int64 First, Last string }
db
packagedb
is the actual implementation of the interaction with the database. This is where SQL statements are constructed and executed. This package also imports model
, p.ch. he will need to create these structures from the database data.
First of all, db
should provide the InitDB
function, which will establish a connection to the database, as well as create the necessary tables and prepare SQL queries.
Our simplified example does not support migrations, but, theoretically, this is where they should be performed.
We use PostgreSQL, which means we need to import the pq driver. We will also rely on sqlx
and we need our model
. Here is the beginning of the implementation of our db
:
package db import ( "database/sql" "github.com/grisha/gowebapp/model" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) type Config struct { ConnectString string } func InitDb(cfg Config) (*pgDb, error) { if dbConn, err := sqlx.Connect("postgres", cfg.ConnectString); err != nil { return nil, err } else { p := &pgDb{dbConn: dbConn} if err := p.dbConn.Ping(); err != nil { return nil, err } if err := p.createTablesIfNotExist(); err != nil { return nil, err } if err := p.prepareSqlStatements(); err != nil { return nil, err } return p, nil } }
The exported InitDb()
function creates an instance of pgDb
, which is the Postgres implementation of our model.db
interface. It contains everything you need to communicate with the database, including prepared queries, and implements the methods necessary for the interface.
type pgDb struct { dbConn *sqlx.DB sqlSelectPeople *sqlx.Stmt }
Below is the code for creating tables and preparing queries. From the point of view of SQL, everything is rather simplistic and, of course, there is room for improvement:
func (p *pgDb) createTablesIfNotExist() error { create_sql := ` CREATE TABLE IF NOT EXISTS people ( id SERIAL NOT NULL PRIMARY KEY, first TEXT NOT NULL, last TEXT NOT NULL); ` if rows, err := p.dbConn.Query(create_sql); err != nil { return err } else { rows.Close() } return nil } func (p *pgDb) prepareSqlStatements() (err error) { if p.sqlSelectPeople, err = p.dbConn.Preparex( "SELECT id, first, last FROM people", ); err != nil { return err } return nil }
Finally, we need to provide a method that implements the interface:
func (p *pgDb) SelectPeople() ([]*model.Person, error) { people := make([]*model.Person, 0) if err := p.sqlSelectPeople.Select(&people); err != nil { return nil, err } return people, nil }
Here we take advantage of sqlx
to execute the query and build a slice from the results, simply by calling Select()
(Note: p.sqlSelectPeople
is of type *sqlx.Stmt
). Without sqlx
we would have to iterate through the rows of the result, processing each with the help of Scan
, which would be more verbose.
Beware of one very subtle point. people
could be defined as var people []*model.Person
and the method would work the same way. However, if the database returns an empty rowset, the method returns nil
, rather than an empty slice. If the result of this method is later encoded in JSON, then it will become null
, not []
. This can cause problems if the client side does not know how to handle null
.
That's all for db
.
ui
packageIn the end, we need to serve all of this via HTTP, and this is exactly what ui
does.
Here is a very simplified version:
package ui import ( "fmt" "net" "net/http" "time" "github.com/grisha/gowebapp/model" ) type Config struct { Assets http.FileSystem } func Start(cfg Config, m *model.Model, listener net.Listener) { server := &http.Server{ ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16} http.Handle("/", indexHandler(m)) go server.Serve(listener) } const indexHTML = ` <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>Simple Go Web App</title> </head> <body> <div id='root'></div> </body> </html> ` func indexHandler(m *model.Model) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, indexHTML) }) }
Note that indexHTML
almost nothing. This is almost 100% all the HTML that our application will use. It will change a little when we proceed to the client side of the application, just a few lines.
Also note how the handler is defined. If this idiom is not familiar to you, it is worth spending a few minutes (or a day) to absorb it completely, since it is very common in Go. indexHandler()
is not the handler itself, it returns a handler function. This is done this way so that we can pass *model.Model
through the closure, since the signature of the HTTP handler function is fixed and the pointer to the model is not one of its parameters.
While we in indexHandler()
do nothing with the pointer to the model, but when we get to the actual implementation of the list of people, we need it.
The above is, in fact, everything you need to know to create a basic web application on Go, at least from Go. In the next article I will deal with the client part and we will complete the code of the list of people.
Source: https://habr.com/ru/post/329584/
All Articles