Go Web Programming: MVC architecture based web app
In this article I want to talk about how to structure your MVC based app so its reusable and robust. Much of the style you are going to see in this article is based on Django (Python web framework).
Let’s start, are we going to use a framework ? No, we are going to use GoLang’s inbuilt net/http
package at the core in conjunction with some handy tools from Gorilla (web tool kit). Throughout the article I’m also going to talk about the development environment in which we are going to build a tiny post publishing web app.
First things first, we are going to use a REPL (read, eval, print, loop) in the form of gin, a live build server tool which will recompile your Go app when there are any changes to the code base.
To install just do go get https://github.com/codegangsta/gin from your app directory
.
We are also going to need a package dependency manager, for this I’m going to use GoDep. Godep is also smart enough to create a vendors
directory and load third party packages into that folder, this is required if you want to containerize the app.
Just like earlier, to install, go get https://github.com/tools/godep .
Right, now that the environment is sorted, lets talk about the folder structure.
That’s a lot of folders, I’ll cover most of them as we go along but for now the most important folders to note are
templates
— For holding our html
controllers
— For holding our controllers
models
— For holding our data models
medium.go
is going to the starting point of our application
Finally, its code time, without wasting any time lets dive into medium.go
package mainimport (
"log" // System"medium/routers" // Local"github.com/urfave/negroni" // Third Party
)func main() {
router := routers.GetRouter()
n := negroni.Classic()
n.UseHandler(router)
log.Println("Listening:")
n.Run(":3001")
}
Looking at the imports, having an import order is really useful for others to understand. Here, we use negroni, a tiny middleware focused library which works directly with Go’s net/http
. n.Run(":3001")
will run our app on localhost:3001
, since we are using gin, from our app directory lets run gin --port:3000
, this will run a proxy server for our live reload server. Now our web app can be accessed either from ports 3000 or 3001. Of course the whole point of gin is to use it from 3000. Using gin I can skip running / building the project every time I make a change.
To install negroni, go get https://github.com/urfave/negroni
Now, what does the router variable hold? Good question, lets dive into router.go
.
package routersimport (
"net/http""medium/controllers"
"medium/middlewares"
"medium/urls""github.com/gorilla/pat"
"github.com/urfave/negroni"
)// registers all routes for the application.
func GetRouter() *pat.Router {
// url paths imported from urls package
url_patterns := urls.ReturnURLS()
medium := pat.New()
medium.Get(url_patterns.HOME_PATH, controllers.HomeController)
common := pat.New()
// static route
common.PathPrefix(url_patterns.STATIC_PATH).Handler(
http.StripPrefix(url_patterns.STATIC_PATH, http.FileServer(http.Dir("static"))))
// applying middlewares
common.PathPrefix(url_patterns.MEDIUM_PATH).Handler(
negroni.New(
negroni.HandlerFunc(
middlewares.LoggingMiddleware),
negroni.Wrap(medium),
),
)
common.Get(url_patterns.LOGIN_PATH, controllers.LoginController)
common.Post(url_patterns.LOGIN_PATH, controllers.LoginController)
return common
}
Ok this file has lots of things going on, a variable url_patterns
is holding values returned from the urls
package.
package urlstype (
urls struct {
STATIC_PATH string
LOGIN_PATH string
HOME_PATH string
MEDIUM_PATH string
}
)func ReturnURLS() urls {
var url_patterns urls
url_patterns.STATIC_PATH = "/static/"
url_patterns.LOGIN_PATH = "/"
url_patterns.MEDIUM_PATH = "/medium/"
url_patterns.HOME_PATH = url_patterns.MEDIUM_PATH + "home/"
return url_patterns
}
This is the urls
package, here we define all the urls we want our application to have access to, the urls defined here are going to be used in the routers
, controllers
, templates
packages. This way we have one file where we can manage/edit urls throughout the app. Any url we want to add to the application needs to be done here.
Going back to the routers
package, after we have retrieved all the urls, we start to construct routes that are associate with controllers.
common := pat.New()
common.Get(url_patterns.LOGIN_PATH, controllers.LoginController)
common.Post(url_patterns.LOGIN_PATH, controllers.LoginController)
Here common is a pat object, pat
is a request router and dispatcher which uses regex under the hood to match urls requests.
To install pat, go get https://github.com/gorilla/pat
At this point, we have installed quite a few packages, lets use GoDep
to save the packages to a file, we can do this by hitting godep save
, this will do a bunch of things, first of, it will create a Godeps
directory with a json file containing all the packages required in the application including the package versions. Under any circumstance do we lose the packages from our $goroot
or $gopath
we can easily restore using the json by hitting godep restore
, handy !!
Anyways, pat
, has all your basic http
methods like get, post, put, patch, delete. I have decided to use only get and post for this demo.
common.Get(url_patterns.LOGIN_PATH, controllers.LoginController)
As you can see, each route will look something like this, i.e,
<router object>.<http method>(<url pattern>, <controller the request has to be sent to>)
Any route that you want to add to the application needs to be done here. And to do so we need a url and a controller.
One more thing, I want to talk about in the router.go
file before I head onto controllers, is the following two entries,
// static route
common.PathPrefix(url_patterns.STATIC_PATH).Handler(
http.StripPrefix(url_patterns.STATIC_PATH, http.FileServer(http.Dir("static"))))
To serve static files, like JS, CSS and images, we use this route.
This route basically says any request with url that starts with the “static” prefix means it is going to look for a file in the static
directory to be served directly.
// applying middlewares
common.PathPrefix(url_patterns.MEDIUM_PATH).Handler(
negroni.New(
negroni.HandlerFunc(
middlewares.LoggingMiddleware),
negroni.Wrap(medium),
),
)
What if we need every request that comes to our web server to undergo some processing? This is where negroni middlewares come in. Here we are saying any request with url that starts with medium
needs to be sent to LoggingMiddleware
first.
Let’s have a peek into middlewares.go
,
package middlewaresimport (
"fmt"
"net/http"
"os"
"strings"
"time"
)func LoggingMiddleware(res http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
// Logic to write request information, i.e. headers, user agent etc to a log file.
next(res, req)
}
I have removed the actual implementation as that is not what we want to discuss here. The link to the entire repo is at the bottom of the article.
The LoggingMiddleware
takes a request and response objects as arguments and then uses the same objects to redirect to a controller after finishing some task.
Controllers
Controllers
, here we go
Every entry in the router.go
file is mapped to a controller in the controllers
package.
package controllersimport (
"log"
"net/http"
"time""medium/helpers"
"medium/models"
"medium/store"
"medium/templates"
"medium/urls"
"medium/utils""github.com/gorilla/schema"
)func LoginController(res http.ResponseWriter, req *http.Request) {
data := make(map[string]interface{})
controllerTemplate := templates.LOGIN
url_patterns := urls.ReturnURLS()
if req.Method == "GET" {
utils.CustomTemplateExecute(res, req, controllerTemplate, data)
}
if req.Method == "POST" {
err := req.ParseForm()
user := new(models.User)
decoder := schema.NewDecoder()
err = decoder.Decode(user, req.Form)
if err != nil {
utils.CustomTemplateExecute(res, req, controllerTemplate, data)
} else {
session, _ := utils.GetValidSession(req)
session.Values["nickname"] = helpers.StripWhiteSpaces(req.Form["Nickname"][0])
session.Save(req, res)
http.Redirect(res, req, url_patterns.HOME_PATH, http.StatusSeeOther)
}
}
}
A controller takes a response and a request object.
The data
variable is a map of string keys holding any interface value. Will talk about this in a bit. The controllerTemplate
is a string const from the templates
package, holding the path to a html file. url_patterns
holds the urls from the urls
package.
Here is the templates.go
file, holding the html paths,
package templatesconst BASE string = "templates/base.html"
const HOME string = "templates/home.html"
const LOGIN string = "templates/login.html"
const POSTS string = "templates/posts.html"
const POST_ADD string = "templates/post_add.html"
const POST_VIEW string = "templates/post_view.html"
Now, every controller is divided into get and post, in the LoginController
the get request handler calls utils.CustomTemplateExecute
from the utils
package. Any template that needs to be added to the application needs to be done here.
The utils
package holds any common functionality throughout the app which helps in the flow of the app
Here is the utils.go
file,
package utilsimport (
"net/http"
"strings"
"text/template""medium/models"
"medium/templates"
"medium/urls""github.com/gorilla/sessions"
)func CustomTemplateExecute(res http.ResponseWriter, req *http.Request, templateName string, data map[string]interface{}) {
// Append common templates and data structs and execute template
tempFunc := template.FuncMap{
"add_params": func(url string, params ...string) string {
for _, param := range params {
// not proud of this
if strings.Contains(url, "placeholder") {
url = strings.Replace(url, "placeholder", param, 1)
} else {
url = strings.Replace(url, "{"+param+"}", "placeholder", 1)
}
}
return url
},
}
data["url_patterns"] = urls.ReturnURLS()
session, _ := GetValidSession(req)
data["nickname"] = session.Values["nickname"].(string)
t, _ := template.New("").Funcs(tempFunc).ParseFiles(templates.BASE, templateName)
t.ExecuteTemplate(res, "base.html", data)
}
CustomTemplateExecute
is a function that takesa request and response object as arguments and additionally the data
map variable which we just talked about, and the template path.
The values from data
map variable is now available to be used in a html file via the string key.
The CustomTemplateExecute
then calls for the template to be executed after parsing the html files.
Now, lets talk about the post part of the LoginController
,
Post normally takes form data which is parse by calling req.ParseForm()
,
Form data is now available for us to use,
err := req.ParseForm()
user := new(models.User)
decoder := schema.NewDecoder()
err = decoder.Decode(user, req.Form)
The above snippet from LoginController
uses the schema package to map Form data to a User
struct from models
package.
Let us see how the form and model look like,
<form class="c-form" action="" method="post">
<div class="input-field col s12">
<input id="nickname" name="Nickname" type="text" class="validate" required>
<label class="active" for="nickname">Nickname</label>
</div>
<div class="input-field col s12">
<input class="btn blue" type="submit" value="Enter" />
</div>
</form>
models.go
package modelsimport (
"time"
)type (
User struct {
Nickname string
}
)
As simple as that, we have a form with a text input of “Nickname” and a model with a “Nickname” field. Schema does a great job at mapping and now we can use the form value as user.Nickname
. Any models that needs to added the application needs to be done here.
Note: Any changes to the html field from the user will yield in a schema error. No tampering !!
Now
err = decoder.Decode(user, req.Form)
if err != nil {
utils.CustomTemplateExecute(res, req, controllerTemplate, data)
} else {
session, _ := utils.GetValidSession(req)
session.Values["nickname"] = helpers.StripWhiteSpaces(req.Form["Nickname"][0])
session.Save(req, res)
http.Redirect(res, req, url_patterns.HOME_PATH, http.StatusSeeOther)
}
If err
is not nil we re render the html, else we create a session with the value of “Nickname”. We are using a method here from the helpers
package.
helper.go
package helpersimport "strings"func StripWhiteSpaces(whiteSpacedString string) string {
return strings.Join(strings.Fields(whiteSpacedString), "")
}
This helper function helps in removing any white spaces from the Form’s “Nickname” value.
Any function that helps a controller with it’s flow is put in the helpers
package. We need to use our best judgement to see if a function belongs to the helpers
package or the utils
package. I normally put domain/controller specific functions in helpers
and application/system specific functions in utils
.
If you have observed, we just came across a very important topic, Sessions !
A function in the utils
package is responsible for creating a session to be accessed and used throughout the application
func GetValidSession(req *http.Request) (*sessions.Session, error) {
// Returns a valid authenticated user session
sessStore := sessions.NewCookieStore([]byte("medium"))
return sessions.Store.Get(sessStore, req, "medium")
}
sessions is again a Gorilla package.
Note: NewCookieStore
takes an argument to encrypt, loading a hash string from the environment is a good idea.
Templates
That controller had a lot of things in it, lets move on to templates
Go has template inheritance and other handy functionalities built into the text/template
package. Let’s talk about template inheritance,
Some templates I have defined,
base.html
— The common template for all templates, includes CSS, JS, Nav Bar etc
home.html
— Welcomes the user, and contains link to adding a post
posts.html
— Shows all the posts
base.html
<!DOCTYPE html>
<head>
<title> { Medium } </title>
</head>
<body class="grey lighten-2">
<nav>
<div class="nav-wrapper green darken-1">
<a href="#" id="title-text" class="c-no-pointer center brand-logo">{ Medium }</a>
<ul id="nav-mobile" class="left hide-on-med-and-down">
</ul>
</div>
</nav>
<div class="">
<div class="row">
<div class="col s1">
</div>
<div class="col s10">
{{ template "content" .}} // content block
</div>
<div class="col s1">
</div>
</div>
</div>
<!-- JavaScript Libraries -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js">
$(document).ready(function () {
// Initialize collapse button
$(".button-collapse").sideNav();
});
</script>
</body>
</html>
base.html
is rendered in every call of utils.CustomTemplateExecute
t, _ := template.New("").Funcs(tempFunc).ParseFiles(templates.BASE, templateName)
In the above line, templateName
is passed from a controller and is basically inserted in the template "content"
block of base.html
login.html
{{ define "content" }}
<div class="col s12 card c-padding-top-20 c-padding-bottom-10">
<div class="col s4">
<form class="c-form" action="" method="post">
<div class="input-field col s12">
<input id="nickname" name="Nickname" type="text" class="validate" required>
<label class="active" for="nickname">Nickname</label>
</div>
<div class="input-field col s12">
<input class="btn blue" type="submit" value="Enter" />
</div>
</form>
</div>
</div>
{{ end }}
This is how login.html
would look, starts of by defining “content” block.
For every block that is declared in base.html
needs to be defined in login.html
or any other template passed from the controller.
home.html
{{ define "content" }}
<div class="col s12 card c-padding-top-20 c-padding-bottom-10">
Hello, {{.nickname}}
<a href="{{.url_patterns.POST_ADD_PATH}}" class="btn blue right">Add Post</a>
</div>
{{ end }}
Looking at home.html
we can see we can use urls from the urls
package.
As we are sensing, any {{ }}
in html is replaced by values from data
map variable from the utils.CustomTemplateExecute
function.
session, _ := GetValidSession(req)
data["nickname"] = session.Values["nickname"].(string)
The above snippet from utils.CustomTemplateExecute
gives us access to “Nickname” in any template.
Nifty !
So far so good, what about REST urls you might ask ? Let’s head on to
posts.html
{{ define "content" }}
<div class="row">
{{$urls := .url_patterns}}
{{ range .posts }}
<div class="col s12 card">
<h5><a href='{{add_params $urls.POST_VIEW_PATH "postid" .ID.Hex }}'>{{.Text }}</a></h5> <h6 class="right"> by {{.Nickname}} on {{.Time}} </h6>
</div>
{{ end }}
</div>
{{ end }}
posts.html
contains any posts we have submitted, clicking on a post will take us to the specific post page post_view.html
Not fancy, but if you observe the url, this was basically generated by add_params
function.
add_params
is template function that we declared in utils.CustomTemplateExecute
, any functions defined there would be available for us to use in html.
The function add_params
,
tempFunc := template.FuncMap{
"add_params": func(url string, params ...string) string {
for _, param := range params {
// not proud of this
if strings.Contains(url, "placeholder") {
url = strings.Replace(url, "placeholder", param, 1)
} else {
url = strings.Replace(url, "{"+param+"}", "placeholder", 1)
}
}
return url
},
}
posts.html
<a href='{{add_params $urls.POST_VIEW_PATH "postid" .ID.Hex }}'>
The first argument is the url from the urls package
, in this case
url_patterns.POST_VIEW_PATH = url_patterns.MEDIUM_PATH + "post/{postid}/"
The next two arguments need to be the param name i.e postid
followed by the value.
Right, now I want to talk about some snippets from the app which I feel are important to note,
Redirection
Redirection from a controller can be made using a simple statement,
http.Redirect(res, req, url_patterns.HOME_PATH, http.StatusSeeOther)
What if we need to pass some params ?
We use the AddParamsToUrl
function from, you guessed it, the utils
package.
func AddParamsToUrl(url string, args []models.Kwargs) string {
// Add params to url using a splice of models.kwargs struct
for _, arg := range args {
url = strings.Replace(url, "{"+arg.Key+"}", arg.Value, 1)
}
return url
}
We can use the above function in the following way,
var kwargs []models.Kwargs
kwargs = append(kwargs, models.Kwargs{Key: "postid", Value: <post id>)})
http.Redirect(res, req, utils.AddParamsToUrl(urls.POST_VIEW_PATH, kwargs), http.StatusSeeOther)
What is models.Kwargs
?
package modelstype (
// Pass keyword args to urls
Kwargs struct {
Key string
Value string
}
)
Kwargs is a struct in models.go
, which has two fields, Key
and Value
.
Key
is the param name in the url_patterns
, and Value
is the value you the param name to be replaced with, in this case {postid}
.
Data persistence:
This is out of the scope of this article but I am just going the pen down my thoughts here. There are ORM’s for SQL based databases like gorm, there are mature drivers for NoSQL databases as well like mgo for MongoDB.
Personally, I have used mgo and wrote my own persistence layer which can be found in the store
package.
Final Thoughts
Following this architecture / blueprint helps in easily understanding the flow of the application and modularizes the app into components for robust development.
There are enough packages and tools out there to build powerful Go Web apps. There are also several frameworks out there which do a job but building your own blueprint doesn’t take too long, and gives you the ease of owning your entire code base.
I have tried to cover the most important parts of the app, but also managed to leave some parts out. I strongly encourage you to clone the repository and give it a test run. Go through the code and fiddle around with it.
Link to repo: https://github.com/priyankcommits/medium
Steps to run:
- Clone repo
- Install
godep
,go get https://github.com/tools/godep
- Install
gin
,go get https://github.com/codegangsta/gin
- Install dependencies by hitting
godep save
(go get -v
also works) - Run using
gin --port:3000
- Point browser to
localhost:3000
That’s about it for now, do let me know what you think and I will be glad to answer any questions.
Lastly, Write in GO !!!