Controllers and Views in Go

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.

Controller & Action Structs

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.

View Struct & Template Folder Structure

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.

Template Folder Structure

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

View Struct

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.

Using the Controller & View

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.

Cats View

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.

Cats Controller

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.

Cats Server

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.

In Conclusion…

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.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes three chapters from the book, and over 2.5 hours of screencasts.

You will also receive notifications when I release new articles, along with other freebies that I only share with my mailing list.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who also teaches about Go, web development, algorithms, and anything programming related. He also consults for other companies who have development needs. (If you need some development work done, get in touch!)

Jon is a co-founder of EasyPost, a shipping API that many fortune 500 companies use to power their shipping infrastructure, and prior to founding EasyPost he worked at google as a software engineer.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Vote on Hacker News

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.

Recent Articles All Articles Mini-Series Tags About Me Go Courses

©2018 Jonathan Calhoun. All rights reserved.