DEV Community

Jon Calhoun
Jon Calhoun

Posted on • Originally published at calhoun.io

Using MVC to Structure Go Web Applications

This article is part of a larger series on Go application structure that was originally posted on calhoun.io, where I write about Go, web dev, testing, and more. I will be porting the entire series over to Dev.to, but I appreciate you checking out on my website anyway šŸ˜€

What is MVC?

As you learn to program, you will eventually start to notice that some code bases are easy to navigate while others are confusing and hard to maintain or update. At first it is hard to tell what makes the difference, but over time you will quickly learn that the biggest factor is how well organized and structured the code is.

The reason for this is simple - if you need to jump in and start adding a new feature, fixing a bug, or doing anything else to a program it is much easier to do if you can guess where in the code you need to start looking. When you have no idea where to even start, you are left digging through hundreds of source files, but in a well structured program you can often guess where the code is even if you havenā€™t read the source code in the past.

Model-View-Controller, commonly referred to as MVC, is a pattern used to help organize and structure code. Specifically, most code will be classified as a model, view, or controller.

Views

Views are responsible for rendering data. Thatā€™s it. Given a specific page that we want to render, and data for that page, our view is responsible for generating the correct output. If we are implementing an application using MVC, this will typically be server-rendered HTML that we want to return to the end userā€™s browser, but it doesn't have to be. Our view could just as easily handle rendering XML, JSON, or other data types.

The important thing to remember is that the code in a view should have as little logic going on as possible; instead, it should focus entirely on displaying data. If logic starts to creep into your views you are likely doing something wrong, and it could become problematic down the road.

In my applications I like to have some global helper functions available in all templates that aren't provided by the html/template package. For instance, it is very common to want to render the current user's avatar or email address in the navbar, but providing this data in every single template render can be tedious. Or I may want helpers for rendering HTML forms via Go structs using something like my form package. None of these extra functions are a requirement of a view package, but I find them helpful when thinking about what my view is providing.

Controllers

Air traffic controllers are the people that inform each plane at an airport where to fly, when to land, and on which runway to land. They donā€™t actually do any piloting, but instead are in charge of telling everyone what to do so that an airport can operate smoothly.

Controllers in MVC are very similar to air traffic controllers. They won't be directly responsible for writing to a database or creating HTML responses, but instead they will direct incoming data to the appropriate models, views, and other packages that can be used to complete the work requested.

Similar to views, a controller shouldnā€™t be packed with too much business logic. Instead, they should mostly just parse data and ship it off to other functions, types, etc to handle.

Models

Models are the final part of MVC, and their primary responsibility is interacting with your application's data. This typically means communicating with your database, but it could also mean interacting with data that comes from other services or APIs. It might also mean validating or normalizing data.

A quick example - most web applications will have some notion of a user who signs up and interacts with the web application in some way. In an MVC application we might create a type User struct {...} in the models package that represents a user stored in our database. We might then create some code to help us validate that a user is valid and some additional code to help us create, update, and query for users in our database.

One important distinction that I'd like to make here is that I consider the models package to encompass database interactions. As we dive into some potential problems with MVC, we will look at how not doing this can lead to some potential problems.

MVC can be implemented in a variety of ways

When we talk about MVC it is important to note that MVC doesn't have a single specific implementation. It is a general architectural pattern, not a specific set of rules we must follow. As a result, two developers using MVC might end up with very different structures.

Part of this is because there are really two aspects of an app's structure that we need to consider:

  1. How we organize code into types/funcs.
  2. How we package up those types/funcs.

While MVC definitely gives us guidance on how to organize our code into types/functions, it doesn't necessarily dictate that we package our code into models, views, and controllers directories. As a result, I would consider the following two examples to both use MVC.

Example 1: Packaging our code based on it's layer

app/
  models/
    user.go
    course.go
  controllers/
    user.go
    course.go
  views/
    user.go
    course.go

In this first example we are organizing our code into packages based on its "layer" - or in other words, we package all the views together, all the controllers together, and so on. This is by far the most common layout you will see with MVC.

Example 2: packaging our code based on the resource

app/
  user/
    controller.go
    model.go
    store.go
    view.go
  course/
    controller.go
    model.go
    store.go
    view.go

In the second example we are opting to package our code based on the resource, and then inside of each package we have our model, view, and controller. This is less common for an MVC application, but I would still consider it to be MVC.

Sidenote: I consider both model.go and store.go to be the "models" part of MVC in this example.

Using that same line of reasoning, it is possible to use a flat structure and MVC at the same time.

Why do people like MVC?

People don't like seeing 30+ source files in a single directory/package. It just feels cluttered, regardless of whether or not the files are all separated in a logical way or not. I'm not really sure why this is the case, but developers just want to start adding subdirectories and "organizing" their code.

Without a deep understanding of what you are building, it can be challenging to break your application up into logical packages. While MVC itself doesn't provide a specific solution to this problem, MVC does provide us with an initial set of layers to start with. If we opt to organize our code into packages based on these layers (see Ex1 above) we can start with the models, views, and controllers directories without really having a deep understanding of what we are building. In other words, it allows us to pick a structure right away rather than waiting for one to evolve, and many developers don't like waiting.

MVC is also appealing because it is familiar. Change can often be confusing, so when coming to Go from Django, Rails, ASP.NET, and countless other languages/frameworks that use MVC it feels comfortable to dive into something familiar. We already understand how that code is organized, so doing something similar with our Go application allows us to make one less decision, and decision fatigue is a real thing.

Lastly, MVC is a pretty natural evolution from a flat-structured application where we often break our code into a database, handler, and rendering layer anyway. All three of these map pretty nicely to models, controllers, and views. More on this later.

Using MVC effectively

Contrary to what you may read on Reddit, MVC can be used quite effectively in Go. Frameworks like Buffalo take advantage of the MVC structure, and I have successfully built several applications using the MVC pattern.

Not everything is a model, view, or controller

The first key to using MVC with Go is to remember that not everything has to be a model, view, or controller. Yes, these categorizations are great starting points and will help minimize your decision making, but many parts of your application will not fall into any of these categories and that is okay.

For instance, in my course Web Development with Go we use the MVC pattern because I find it to be a very useful learning tool (more on that later), but it doesn't take very long before we start introducing additional packages. One example is middleware that are related to handling HTTP requests, but don't really fit into the controller package. When this occurs, it is okay to create additional packages in your codebase.

Don't break things up too much

The second key to using MVC is to avoid breaking things up too much for no real reason. I have seen a few people try to implement MVC this way:

app/
  models/
    lesson/
      models.go
      store.go
    course/
      models.go
      store.go
  # and so on...

The problem with this structure is that models tend to be relational; that is, a Course may have many Lessons, so we might want to write queries that return a course along with all of its lessons, and in the structure I just showed this typically requires additional types (like a CourseLesson) to avoid cyclical dependencies.

I have heard of developers having success with this pattern, so I'm not suggesting that it should never be used, but I personally haven't found it to fit my needs very well. As a result, it just isn't what I do.

If you happen to have examples of where this pattern works well feel free to send them to me - jon@calhoun.io

More generally speaking, if you find yourself with cyclical dependencies there is a good chance you either broke things up a bit too much or put a type/interface/func in the wrong place. Take Kat Zien's layered example from her talk, How do you structure your Go apps; in this example the code won't compile because the storage package and the models package have cyclical dependencies. While this seems to point with an issue in the application structure, I would actually argue that the models and storage package in this example should really be one package because they both pertain to storage. The models in this case just happen to be definitions of the types stored in said storage.

To be very clear - I am in no way criticizing Kat's ability to structure applications. I believe she created this cyclical dependency intentionally to make a point in her talk, and it serves as a great example for me as well. Oh and you should totally go check out her talk if you haven't already! šŸ‘‰ Kat's Talk on YouTube

How should I structure my MVC apps?

Whether I am using MVC, Domain Driven Design (DDD), or another design pattern, I almost always prefer to package my code by the layer that it serves in my architecture. That is basically just a fancy way of saying I like to put all of my database code together in one package, my middleware in one package, my handlers in one package, and so on.

Sidenote: There are definitely times where packaging code in another way makes sense, so don't take this to mean you should always package by layer. This is just what tends to work well for me.

In MVC, that looks basically identical to what you would probably expect in an MVC application:

app/
  models/
    # user.go has all my database logic including:
    #   User - the model definition
    #   UserStore - used to perform user-related CRUD
    #     operations on the DB
    user.go
    course.go
  controllers/
    user.go
    course.go
  views/
    template.go # used to parse gohtml templates
    show_user.gohtml
    new_user.gohtml
    # ...

The only real caveat is that you don't really need to name these folders models, controllers, and views. You can, and that is perfectly fine by me if it makes development easier on you in any way, but you can also name these in a slightly more specific way. For instance, if our models are all stored in a PostgreSQL database, our controllers are all intended to handle HTTP requests, and our views are all used to render HTML, we might end up with packages like this:

app/
  psql/ # models
  http/ #controllers
  html/ #views

Now it is a little more clear that the psql.User represents a user in our Postgres database, and an http.UserHandler is pretty obviously a handler for http requests pertaining to users.

That doesn't mean you always have to do this. There are times when I have felt that a models package makes more sense. For instance, if I know my application is going to be fairly small and one of my models will be a file stored on disk (eg an image), I might opt to name my package models and mix-n-match all of my database and local storage code into that one package.

On the other hand, you could opt to break these two things into separate packages where one is psql specific, and the other is named localdisk or something like that.

I don't see a lot of value in criticizing someone's choice here because which decision is best almost always depends on the size and complexity of the application. What is more important to me is that you understand the tradeoffs you are making and are okay with them.

Lastly, it is worth mentioning that sometimes the reason for a package name is entirely unrelated to development/code. For instance, I prefer keeping the models package name in my web development course because this often makes it easier for people to grasp how we are separating our code, and thus makes it easier for them to pick up Go.

What goes in the models / sql package?

Models is probably the most important package to get right, so we are going to start with it. For simplicity's sake, let's assume that only means SQL database interactions.

The models package should contain pretty much all of your code related to your data storage. That includes database-specific models, validations, normalizations, etc. It is also worth recognizing that this package shouldn't generally be importing other packages in your application. As with all rules, there are exceptions, but I find that the models package works best if you could literally pull it out of your application and use it in another application (say a CLI) without changing any of the code in the models package. In this sense, all of your models code ends up being isolated and is limited to ONLY interacting with database entities. This will make it basically impossible to introduce cyclical dependencies because your models package doesn't know anything about the rest of your application.

Here are a few examples of logic I might expect to find in a models package:

  1. Code that defines the User type as it is stored in the database.
  2. Logic that will check a user's email address when a new user is being created to verify that it isn't already taken.
  3. If your particular SQL variant is case sensitive, code that will convert a user's email address to lowercase before it is inserted into the DB.
  4. Code for inserting users into the users table in the DB.
  5. Code that retrieves relational data; eg a User along with all of their Reviews.

What I wouldn't expect to find in a models package is code specific to rendering your HTML responses. Eg I wouldn't expect it to return errors with HTTP status codes. You can return an error like NotFound but the controller should be free to interpret and render that as it sees fit. In the context of HTTP that might end up being an http.StatusNotFound status code, but in the context of a CLI it could be entirely different.

What goes in the views package?

This one tends to be the least code because it is usaully either:

  • A glorified wrapper around the html/template.Template package
  • A wrapper (or direct use of) the encoding/json package

The main way I tend to customize this is by adding a bunch of template.FuncMap functions to the template BEFORE parsing it (so it parses correctly), and then I'll override them using request-specific data if I need to. Eg:

package html

type Template struct {
  template *template.Template
}

func ParseFiles(files ...string) (*Template, error) {
  tpl, err := template.New("").Funcs(template.FuncMap{
    "flash": func() string { return "" },
  }).ParseFiles(files...)
  if err != nil {
    return nil, fmt.Errorf("parsing view files: %w", err)
  }
  return &Template{
    template: tpl,
  }, nil
}

func (t *Template) Execute(w http.ResponseWriter, r *http.Request, data interface{}) {
  // clone the template BEFORE adding user-specifics!
  clone := t.clone()

  // Get the flash cookie
    cookie, err := r.Cookie("flash")
    switch err {
  case nil:
    // override the flash msg
    clone.template = clone.template.Funcs(template.FuncMap{
      "flash": func() string { return cookie.Value },
    })
        // Delete the flash cookie so we don't repeat it.
        expired := http.Cookie{ ... }
        http.SetCookie(w, &expired)
    default:
        // noop
    }

  err = clone.template.Execute(w, data)
    if err != nil { ... }
}

Sidenote: I talk about some additional ideas to try with the view package in an older article Creating the V in MVC. I'm not saying I'd recommend them all at this point, but they are worth looking over as a mental exercise.

What goes in the controllers package?

If you get the other two right, this package ends up being pretty boring. You basically just create http.Handlers that parse incoming data, call methods on something like a UserStore or CourseStore provided by your models package, then finally render the results via a view (or redirect the user to an appropriate page in some cases).

// Take note: our UserHandler has a UserStore injected in!
// Global data stores can be problematic long term.
type UserHandler struct {
  Users    models.UserStore
  Sessions models.SessionStore
}

func (uh *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
  err := r.ParseForm()
  if err != nil { ... }

  var form struct {
    Email    string `schema:"email"`
    Password string `schema:"password"`
  }
  schema.NewDecoder().Decode(&form, r.PostForm)
  user := sql.User{
    Email: form.Email,
    Password: form.Password,
  }
  err = uh.Users.Create(&user)
  if err != nil { ... }

  token, err := uh.Session.Create(&user)
  if err != nil { ... }
  // create session cookie using token

  http.Redirect(w, r, "/dashboard", http.StatusFound)
}

You might occasionally find yourself defining custom types to help render views, and to this day I'm still unsure of where the best place is for them. In one sense they are view specific, since your view will rely on a very specific set of data being passed in to render correctly. In that case they should probably be defined inside the views package (or a nested package). On the other hand, your controller could conceivably render data in various formats - HTML, JSON, or XML - so I could also see an argument for placing a creating a custom type that is "returned" from each handler inside the controllers package.

I think both can work, so if you do need custom types pick the one that works for you and roll with it. Even if you end up needing to change this decision, it shouldn't be too hard to refactor.

Don't blindly copy other languages

At this point I hope you are starting to see how MVC can be successfully implemented in Go, but I can't end this article without also discussing some of mistakes you might try to copy from other languages that utilized MVC.

Let's take a second to look at what a model might do in an MVC framework like Ruby on Rails:

  1. While the model usually maps to the DB, it is also expected to serve the entire application's needs (eg UI rendering)
  2. Your application has global access to models and other entities like current_user
  3. Models have methods that trigger additional SQL queries based on relational data

While it is possible to implement many of these "features" in Go, I would never recommend it. So let's look at alternatives.

You will need to define resources more than once

In Rails we might define a model once and then expect it to work across our entire application. There are several ways we do this, but it mostly revolves around:

  1. Creating methods that translate data
  2. Creating wrapper types, like a decorator

While it is possible to create a Go web application where the only time you define your resources is in the models package, it is more likely that you will eventually need to define a second version of each resource. For instance, imagine we were rendering a User for a JSON response - if we were to use the models definition of a user we would need to add all of those json struct tags to the models package even though it has nothing to do with the code there. We would also potentially need to write custom marshalling code there to handle the fact that what we store in the database isn't exactly what we want to render to the user.

You are almost always better off just creating a new type and writing code to translate between the two:

package json // view

type User struct { ... }
package sql // model

type User struct { ... }
package http // controller

func sqlUserToJson(user *sql.User) json.User { ... }

Yes, it is more code, but you (hopefully) didn't pick Go because it was the least verbose language available to you.

Globals are a bad idea

In languages like Ruby or Python your server is most likely handling a single web request at a time. This isn't the fastest way to do things, but it does make development significantly easier because you can create global functions like current_user and know that they only pertain to the current request.

Go is nothing like Ruby or Python in this sense. When a web request comes into your Go server the standard library will spin up a new goroutine to handle it and then start to pass the request along to your handlers. This is incredibly handy from a developers perspective because we don't have to do anything to make our web server work concurrently, but it also means that adding a global function like current_user would lead to all sorts of race conditions.

Even a global database connection, which isn't as error prone, is ill-advised. While you might not have race conditions, a global database will allow SQL queries to be executed from anywhere in your application. If you were to instead just inject your data stores into the rest of your code it is much easier to track where queries are being made from, it is easier to test, and your life will generally just be much better.

func main() {
  userStore := sql.NewUserStore(...)
  // Inject the userStore where it is needed!
  userController := http.NewUserHandler(userStore)

  // ... setup the rest of your app
}

Don't embed DB connections to make relational queries possible

While it is possible to make relational queries work in Go much like they do in dynamic languages, I wouldn't recommend it.

package sql

type OrderStore struct {
  db *sql.DB
}

func (os *OrderStore) ByUserID(id int) ([]Order, error) {
  // ...
}

type User struct {
  ID int
  // ...

  // Don't do this
  db *sql.DB
}

func (u *User) Orders() ([]Order, error) {
  os := OrderStore{u.db}
  return os.ByUserID(u.ID)
}

The reasons are basically the same why we avoid a global database connection; it can lead to all sorts of hidden complexity. I write a bit more about this in my article Subtle issues with ORMs and how to avoid them, but the main takeaway is that when this is possible, a SQL query can literally be triggered from anywhere in your application, not just where you inject the OrderStore, and as a result it can make it very hard to manage your application.

In summary...

We spent the last bit of this article talking about things not to do, but really what I want you to take away from this article are all the things before that. I want you to understand that MVC can, and does, work in Go. That MVC is less about what you name your packages, and more about how you organize your code. If you keep that in mind I suspect you will have no problems using MVC in your next web application.

Want to learn Go?

Interested in learning or practicing Go? Check out my FREE course - Gophercises - Programming Exercises for Budding Gophers.

I also have some premium courses that cover Web Dev with Go and Testing with Go that you can check out as well.

Top comments (0)