Django Intro with Go and Flamingo
The Django Framework has a great intro https://docs.djangoproject.com/en/3.0/intro/ where an example poll application is developed.
In this post we want to build a similar app to show how web applications can be developed using the Go based Flamingo Web Framework.
Please note that Flamingo has no automatic admin CRUD generator, so we will skip this parts in this tutorial.
Part 0: Installation
Please make sure to install Go. Version 13 or newer https://golang.org/doc/install
Part 1: Get Started
Creating a new project with Flamingo is actually straight forward.
We start by defining a new Go Module, the bare minimum we need to get started.
~$ mkdir mysite
~$ cd mysite
~/mysite$ go mod init mysite
~/mysite$ go get -v flamingo.me/flamingo/v3
First we create a main.go
file to kickstart Flamingo. Beside the normal App we will also add the Flamingo requestlogger Module.
Now we can run our application with
~/mysite$ go run main.go serve
Now open http://localhost:3322/. Flamingo will give us an json error response because we don’t have anything set up yet.
The Poll app
The Django intro creates a small Poll app, so we will do the very same.
First we place a folder polls
in our mysite
folder. We start by adding our first view. To be consistent with the Django intro we will name the files in the same way.
We use the same names for files as in the Django intro to be consistent.
We create a views.go
and define our first index
view.
In Flamingo there is no concept of globals, or global state. Therefore we have to bind everything to structs to make sure flamingo can handle them properly. This will add a few more lines in the beginning, but eventually helps a lot to be more flexible.
To open the view we need a route. In Flamingo we create a RoutesModule
which tells Flamingo how to wire URLs and views together.
The first lines are boilerplate code. Essentially we create a new urls
struct which gets an instance of our controller. This might seem useless for now but we will need it later.
The Inject
method tells Dingo, the dependency injection framework, what the urls depends on.
The Routes
method implements the web.RoutesModule
interface and configures the RouterRegistry.
Flamingo does not have a configuration like Django where we can “mount” the package on different base-paths. We can, however, change the URLs later in a project using a
routes.yml
file to rewrite routes.
Last but not least we have to make our Project aware of the routes.
Flamingo uses a module-based approach. We create our module.go
and register the URLs:
In our main.go
we will load the module by appending it in our modules list:
Now we run go run main.go serve
and should see our hello world message on http://localhost:3322/.
Part 2: Database Setup
It’s time to add a Database and get ready to define some models.
Flamingo does not bring a default ORM. Instead we use Gorm, a Go ORM Library. We install it via
go get -v github.com/jinzhu/gorm
in ourmysite
folder, or just let go download it once we added the imports.
To faciliate this we will create a db
package to manage our database setup. The database will be provided by a so-called Provider
.
A quick explanation what we are doing: using the Configure
method we tell Dingo to bind creation of the gorm.DB
instances to a provider.
The provider is called anytime something needs an instance of gorm.DB
(e.g. via an Inject
method).
Eventually we bind it in a scope, in this case Singleton
. The Singleton
scope makes sure that we have only one global instance of that database, not one for every controller/action/handler.
As our polls
Module will depend on the DB module we define it as a dependency.
Dingo will now make sure that every time our polls
Module is loaded the db
Module is loaded prior to that.
Next we define our Models in polls/models.go
:
We want to use Gorm’s AutoMigrate function to handle the database schema. To call it we need to add a CLI command and make our Models available to the DB schema. We gonna use Dingo’s “MultiBinding” concept for that.
In db/module.go
we add the CLI Command, and an empty type for models (just so we can reference them):
This code behaves much similar to the database. We bind a new cobra.Command
(cobra is a framework for CLI commands) and assign it to a provider.
The provider declares it’s own dependencies (e.g. what the provider needs to run properly) which is the gorm.DB
and a list of all registered models.
Actually Gorm could do the AutoMigrate directly in the provider, because it is never destructive. However for this example we will stick to a similar design Django has and want to show how to add new CLI commands, so we keep it like that.
In our polls
app we register the models to the command by “Binding” them:
Now we can setup our Database by running go run main.go migrate
. We check our database locally:
~/mysite$ go run main.go migrate
2020-02-25T16:32:27.363+0100 INFO runtime/module.go:32 maxprocs: Leaving GOMAXPROCS=12: CPU quota undefined {"area": "root", "module": "core.runtime", "category": "module"}
2020/02/25 16:32:27 Migrating *polls.Question
2020/02/25 16:32:27 Migrating *polls.Choice
2020-02-25T16:32:27.369+0100 INFO cmd/module.go:83 start graceful shutdown {"area": "root"}
2020-02-25T16:32:27.369+0100 DEBUG zap/module.go:153 Zap Logger shutdown event {"area": "root"}
2020-02-25T16:32:27.369+0100 INFO v3@v3.1.7-0.20200225135826-b18179097bd4/app.go:391 Shutdown server on :3322 {"area": "root"}
2020-02-25T16:32:27.369+0100 INFO cmd/module.go:100 graceful shutdown complete {"area": "root"}
~/mysite$ sqlite3 test.db
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE IF NOT EXISTS "questions" ("id" integer primary key autoincrement,"created_at" datetime,"updated_at" datetime,"deleted_at" datetime,"question_text" varchar(255),"pub_date" datetime );
CREATE TABLE sqlite_sequence(name,seq);
CREATE INDEX idx_questions_deleted_at ON "questions"(deleted_at) ;
CREATE TABLE IF NOT EXISTS "choices" ("id" integer primary key autoincrement,"created_at" datetime,"updated_at" datetime,"deleted_at" datetime,"question_id" integer,"choice_text" varchar(255),"votes" integer );
CREATE INDEX idx_choices_deleted_at ON "choices"(deleted_at) ;
Great! Django would now use the interactive Django CLI. This is a concept which is not really working with Go, as everything is statically compiled.
For demonstration we will add some demo data right via sqlite:
sqlite> INSERT INTO questions (question_text) VALUES ("Question 1"), ("Question 2"), ("Question 3"), ("Question 4"), ("Question 5"), ("Question 6"), ("Question 7");
sqlite> INSERT INTO choices (choice_text, question_id) VALUES ("Choice 1", 6), ("Choice 2", 6), ("Choice 3", 6), ("Yes", 7), ("No", 7), ("Maybe", 7);
Part 3: More Views
We continue by adding more views to inspect our questions.
To do so we first create the views/handler in our views.go
.
Also we add a web.Responder
to our controller. This is a helper which makes “responding” to web requests much easier.
We extend our urls.go
with the corresponding routes:
Restarting (ctrl+c then go run main.go serve
again) our application allows us to browse the pages, such as http://localhost:3322/123.
Functionality for views
To show a list of questions we gonna need a template and ask the database to give us available questions.
We start by adding gotemplate.Module
to our main.go
to add a template engine.
Afterwards create a template index.html
in a folder templates
:
Flamingo does not bundle templates and modules. This is mainly because in most cases templates are highly project specific.
Next we let the index
view get a list of questions from the database and show them. To do so we add the gorm.DB
as a dependency.
The
viewData
struct is the data we will pass to the template, and which is available at the.
. That make.LatestQuestionList
evaluate to the fieldLatestQuestionList
of theviewData
struct.
We can now see the last 5 questions on http://localhost:3322/.
Our responders Render
function builds a web.RenderResponse
, which renders the given Template (index
). The suffix .html
can be omitted.
Detail Views and 404
Next we will extend out details view, and handle 404 if the given Question was not found.
To render out the details we extend the detail.html
template:
Opening http://localhost:3322/6 or clicking on a question now either gives us the question text, or a 404 message.
Part 4: Forms
Showing Questions is the first step, but we want to interact with the application and be able to vote.
To do so we add a small form to our detail.html
template:
Next we extend our vote
view to handle the submitted post request.
To handle the error cases we use a helper function called
noSelectedChoice
.
Now we finish with the result page where we show the votes.
We add the info to our template in templates/results.html
:
Remove duplicate code
We restructure our polls/views.go
a little to remove duplicate code we use to parse question_id
and check for the entity in Gorm.
A new method viewDataOrError
will try to parse the given ID, check if the entity is available, and either return ready-to-use viewData or a web.Result
with a NotFound or ServerError response.
Part 5: Testing
Most of our functionality is in our views.go
so we will add some tests.
In go we use the suffix _test
for test files, so we create a new polls/views_test.go
file.
Flamingo by default does not use Dingo in tests. We can use it, however it might cause other headaches as we want to focus on tests and not the application in general.
To test/mock our database we add go-sqlmock:
~/mysite$ go get -v github.com/DATA-DOG/go-sqlmock
Our test will set up a test database, then instanciate the controller and Inject the dependencies (web.Responder and gorm.DB).
The empty Responder is fine for testing. We will not render templates here, but we will be able to test if the result of the index()
call returns a render response.
Then we call controller.index(context.Background, web.CreateRequest(nil, nil))
to actually “execute” the request.
The context.Background()
is used as we are in a test context. The web.CreateRequest
method takes two nil
arguments, which makes the request create an empty http.Request
and a empty web.Session
internally.
We can now run our tests with go test ./...
:
~/mysite$ go test ./...
? mysite [no test files]
? mysite/db [no test files]
ok mysite/polls 0.348s
Conclusion
Flamingo is not a replacement for Django, and does not try to be one. Django uses the benefits of the interpreted programming language Phyton, for example providing magic features when it comes to models and extending them with lazy-loading method.
On the other hand a compiled programming language like Go has other advantages such as faster runtime and a smaller memory footprint. What this tutorial shows is that it is possible to achieve similar results when it comes to request handling, usage of databases etc. with the Flamingo Framework.
If you want to learn more about Flamingo or support the project check it out on Github and leave a Star :) https://github.com/i-love-flamingo/flamingo