Chances are a lot of developers who pick up Go will be familiar with the MVC model. Models, views, and controllers are great for abstracting away code, but sadly there aren’t many examples of how to use them in Go.
In this post I am going to go over how to get started with a basic controller and view in Go, and cover a really simple web application that uses them. While I won’t be covering models, it wouldn’t be impossible to write a similar set of packages for models, though the details may vary based on what backend you are using.
The first thing we are going to look at is what we need from our base controller. In a nutshell, what we want is a struct to represent each resource’s controller, such as a UsersController, and actions specific to that controller, such as new and edit. Those two structs could be defined roughly like so
import (
"github.com/julienschmidt/httprouter"
"net/http"
)
type Action func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error
type Controller struct{}
Note - This example is using httprouter, as this is my go-to router in Go. It is built on top of net/http and makes it a little simpler to define routes that are action specific. eg DELETE /users/:id
vs GET /users/:id
With this code we can create a controller and define actions for it, but we still need a way to perform those actions. To handle this we will add a method to the Controller struct.
func (c *Controller) Perform(a Action) httprouter.Handle {
return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
a(w, r, ps)
})
}
Now we have a Perform
method to perform our actions, but it would be nice if this did a little more. For example, if our action returns an error, we should probably catch it and send a 500 status code. That is pretty easy to add.
func (c *Controller) Perform(a Action) httprouter.Handle {
return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if err := a(w, r, ps); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
}
And that pretty much wraps up everything we need for a controller. You can see the full file on GitHub.
Next up is the base view structure that we will use to create our resource views such as the UsersView. In order to do this, we first need to determine the folder structure we are going to use. For simplicity’s sake, I am going to just choose what I prefer and hard code it into my views. You are welcome to change this up as you see fit.
Although this structure can work for smaller/simpler applications, it isn’t what I prefer to use these days. For more info I recommend you check out my course Web Development with Go. It is a lot more comprehensive and is always kept up-to-date.
To start, all HTML templates will be under the directory templates
. Inside of this directory, there will be a folder named layouts
that contains all of our top level layouts. In many cases you will only have one, but this structure will accomodate multiple. Along with the layouts
directory, the templates
directory will also contain a folder for each resource that we create a view for. For example, a UsersView
would use the directory templates/users/
. Inside of the resource directory there may also need to be an includes
directory that defines how parts of the layout
, such as a seconary nav menu, are filled. All said and done it should look something like this with a User
resource:
app/
templates/
layouts/
my_layout.tmpl
other_layout.tmpl
users/
includes/
sidebar.tmpl
index.tmpl
dance.tmpl
Now lets create our view struct. Most resources are going to need an Index, Show, New, and Edit page, so we will go ahead and include those in the default struct, and define the Page struct as well.
import (
"html/template"
"log"
"net/http"
"path/filepath"
)
type View struct {
Index Page
Show Page
New Page
Edit Page
}
type Page struct {
Template *template.Template
Layout string
}
Now we need a method to help us render each page.
func (self *Page) Render(w http.ResponseWriter, data interface{}) error {
return self.Template.ExecuteTemplate(w, self.Layout, data)
}
Lastly, we are going to define a function to help glob all of the layouts in our app. This will be helpful later when we initialize our views.
func LayoutFiles() []string {
files, err := filepath.Glob("templates/layouts/*.tmpl")
if err != nil {
log.Panic(err)
}
return files
}
You can see the fully finished view on GitHub.
Finally we can start using our controllers and views. I am only going to cover the server, the controller, and the view, but you can view a fully functional example app on GitHub.
First lets start with the view.
package views
import (
"github.com/joncalhoun/viewcon"
"html/template"
"log"
"path/filepath"
)
type CatsView struct {
viewcon.View
Feed viewcon.Page // A custom page for the cats view
}
var Cats CatsView
func CatsFiles() []string {
files, err := filepath.Glob("templates/cats/includes/*.tmpl")
if err != nil {
log.Panic(err)
}
files = append(files, viewcon.LayoutFiles()...)
return files
}
func init() {
indexFiles := append(CatsFiles(), "templates/cats/index.tmpl")
Cats.Index = viewcon.Page{
Template: template.Must(template.New("index").ParseFiles(indexFiles...)),
Layout: "my_layout",
}
feedFiles := append(CatsFiles(), "templates/cats/feed.tmpl")
Cats.Feed = viewcon.Page{
Template: template.Must(template.New("feed").ParseFiles(feedFiles...)),
Layout: "other_layout",
}
}
From top to bottom, we first include viewcon, our package with the view and controller structs, as well as any other packages we will need. Next we declare the CatsView
structure, which includes the base View
struct, as well as a custom Feed
page.
Next we declare a publicly accessible CatsView
struct named Cats
. This will be inialized to an empty CatsView since it is not a pointer.
Next up is our CatsFiles()
function that globs all of the included templates in our templates/cats/includes
directory, and joins those with the templates in templates/layouts
.
Finally, we see the init()
function that is used to inialize each page in our Cats
variable (which has the CatsView
type). For each page we append the template file (eg templates/cats/index.tmpl
) specifically for that page, and then create the template, and specify the layout we are using.
Next we need a controller for our Cats resource.
package controllers
import (
"github.com/joncalhoun/viewcon"
"github.com/joncalhoun/viewcon/examples/views"
"github.com/julienschmidt/httprouter"
"net/http"
)
type CatsController struct {
viewcon.Controller
}
var Cats CatsController
func (self CatsController) Index(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error {
// Pretend to lookup cats in some way.
cats := []string{"heathcliff", "garfield"}
// render the view
return views.Cats.Index.Render(w, cats)
}
func (self CatsController) Feed(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error {
// render the view
return views.Cats.Feed.Render(w, nil)
}
func (self *CatsController) ReqKey(a viewcon.Action) httprouter.Handle {
return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if r.FormValue("key") != "topsecret" {
http.Error(w, "Invalid key.", http.StatusUnauthorized)
} else {
self.Controller.Perform(a)(w, r, ps)
}
})
}
From top to bottom, we require the packages we need, declare the CatsController
struct, and create a publicy accessible instace of that controller named Cats
.
From there we create an action method on our CatsController
and name it Index
. This action is pretty simple and just creates a slice of cat names and renders the cats index page with our view from the last section.
Next is the Feed
action. This is pretty much the same as the Index
action.
Finally we create a controller specific method to perform actions named ReqKey
. Typically this would be in another controller, such as an AuthController
, and the CatsController
could include that instead of the base viewcon.Controller
so that you could reuse the authorization code, but for this example we are going to include it directly in our CatsController
.
What ReqKey
does is actually pretty simple. It takes an action, much like Perform
in our viewcon.Controller
, and it returns an httprouter.Handle
function. In this specific case, it will check to see if the param key
was provided and that the value is “topsecret”. If that is not the case, an error is rendered with a 401 status code. Otherwise the base controller’s Perform
method is called to generate the standard httprouter.Handle
function, and then that function is called with w
, r
, and ps
.
Finally we need a server to run everything. Luckily this is one of the easiest parts.
package main
import (
"github.com/joncalhoun/viewcon/examples/controllers"
"github.com/julienschmidt/httprouter"
"log"
"net/http"
)
func main() {
catsC := controllers.Cats
router := httprouter.New()
router.GET("/cats", catsC.Perform(catsC.Index))
router.GET("/cats/feed", catsC.ReqKey(catsC.Feed))
log.Println("Starting server on :3000")
log.Fatal(http.ListenAndServe(":3000", router))
}
The code here simple includes the packages it needs, assignes the publicly accessible controller.Cats
variable (which is of the type CatsController
) to catsC
, and then uses that to define routes that our application responds to.
It may seem daunting at first, but creating your own controllers in Go is actually incredibly easy. If you check out the fully functional example on github.com/joncalhoun/viewcon you will quickly realize that it is easy to extend the code I covered and tweak it as you see fit.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.