Tech Stack Walkthrough

Structuring Golang Applications

Summary

In this video, I address how to structure Golang applications - a common question from newcomers. I start by showing a simple example with a main.go file that handles user creation and retrieval endpoints. As the code grows, I demonstrate how to break it down using MVC (Model-View-Controller) architecture. I show how to separate concerns by moving code into models (for business logic and data), views (for data presentation), and handlers/controllers (for managing incoming/outgoing data). I explain that MVC isn't just about folder organization, but rather a way of thinking about code responsibilities. Throughout, I use a JSON API example to illustrate these concepts.

Transcript

How you should structure Golang applications is one of the most asked questions from people new to Golang. The common answer you receive is something like just start with main.go or do whatever is best suited for your use case or problem domain and let it grow from there. This is true, but if you're new to Golang or new to building applications with Golang, this doesn't really help you, right? Because what is simple when has something grown enough that you need to restructure it? Let me show you an example of starting simple. So here I am in my main.go file in the main function and let's just start by doing an endpoint that creates a user and this might look something like this. So we're gonna start by doing a handler func that sends a POST request to user and in here we need to do a few things. We need to pass data from request. We need to validate the data. We also need to store the user in db and return our response. The response for now is just going to be a JSON data API. So let's set the header to be of content type application JSON and our response we're just going to encode to the response writer a map of type string string rs saying user user created. So if you take a look at the comments, right, we have a few different operations. We're passing some data, validating the data, storing some data and then returning the response. For now, let's just add some placeholder functions for the first three operations. Pass new user request. It's going to take in some data just of type any right now. It doesn't really matter. Return nil and then we're going to validate new user. Let's just say data and then lastly we are going to be saying insert user. Great, but we also would need a database connection. So let's just say func create DB con and it just returns us sql-.db, right? This will not work, but again, this is just placeholder function, so it doesn't really matter. And then we would need to utilize these in our main.go. So let's just say DB con is equal to create DB con and in here we're going to say if error equals to pass nil and I'm just going to handle the error and the same for the validate and also the insert user and the insert user should probably take in a connection as well, right? So we can do DB con like this. It would be nice if you could also see the user we are creating. So to do that we're going to add another placeholder function called query user. It's going to take in a con again from sql.db and then some user ID that's just going to be a string. Let's return a user and an error and the user is just going to be an empty struct. So return user and no. And we just need to add another handler func, so func like so and it's going to be a get request to slash users and then some ID of the user we want to see. Now let's just pull out the user with our placeholder function, say query user and some ID and if there is an error we are going to handle it. Again, let's grab this because we're just going to return our user struct instead of the string here. I'm going to turn any. It's not really important. This is only to illustrate, right? So this is in very simplified terms how you would add the endpoint to get the user out of the database and most of our code right now is hidden in this placeholder function that doesn't really do anything but we're already starting to have quite a few things happening and we only have two operations for one resource. This will work for now what we have right here, even if we didn't have placeholder functions but we don't have to add much more before we want to start at least look at breaking things apart or breaking things up a bit, right? So let's look at what options we have to go from here. There are many ways to start breaking this up and just as many opinions on how to do it. This course will use what is known as a model-view-controller architecture or MVC for short. This is a very well established way of building web apps. You might have heard that MVC is not suited for Go. This is not true. Some think that MVC is all about organizing code into folders. Others think that the only way to create professional Go apps requires you to use domain-driven design with a hexagonal architecture, ports and adapters, etc. But MVC, like other approaches, are mainly a way to think about where code should live and what it should do. MVC architecture provides you with an easy and intuitive way of breaking your applications into singular responsibilities. The controller handles incoming and outgoing data. The view handles how data is presented to the user and the model is in charge of business logic and data. It's about separation of concerns. So let's break a hypothetical data app into MVC architecture. Let's start with models. We have two functions. One to get the user and one to create the user. So let's jump out and create a models directory and then our models.go file and our package is going to be models. Now you typically design your models around structs that aims to mimic your business domain, like what is a user in your application and not necessarily the tables you might have in your data. That's typically what you see in the Rails way, that a model is a one-to-one reflection of what your data table or your database looks like. It's not really how you should think about it, it's how you can think about it, but we want our users to reflect the business domain we're operating in. And that might be a one-to-one with the tables, but not necessarily. And then we have two functions. We have the getUser and then the newUser function. And the getUser, it needs a database connection and then a user ID. Yeah, and it's going to return a user and an error. And we are just going to return an empty user struct and no. Now the newUser function here again takes in the database connection, but now this is where we want our business logic to live. So as you might remember from our main.go file, we had this validate new user data. This should now live in this package, this model package. So in here we would say if error equals validation, something along the lines of this, and let me turn the error. And if not, create, insert the user, and then return. No, right. So we now have shifted our responsibilities of validating the data is what our domain requires to the models. And for now, this is just how we're going to leave it. This will not work. If you try to run this code, this is only for illustration. We would also not really handle database connection this way, but it should give you an idea of what a model is and what it's supposed to do. Next up, let's add our views, which for our JSON data API here is not really going to be reflective of what we're going to do in the rest of the course. So we're just going to add a user response struct. And as I say, this has an ID of type string. Since it's a JSON data API, we're going to be adding a struct tag so we can easily take this structure and put it into a JSON format. For now, let's just leave it like this because in the upcoming videos, we're going to talk more about how we're actually going to handle the view, how we're going to represent data to the user in the actual browser. So not that much happening here right now. But the view, whatever you showcase to the user, if it's in the browser, if it's in the data API, this goes in here. The last thing we need is our controllers. But in Go, you often call controllers handlers. So handlers, controllers, controllers, handlers. I'm going to use those two interchangeably. This is just a convention. So handlers, and again, handlers.go. And then we have this package. Now, let's go in and grab from our main.go file. So this was the create user. And this one was our getUser. And if you look at the signature of our handler file, you can see it expects a function with the signature of a response writer or a request. So the way to do this in our handlers here is that we're going to simply say it's going to return a function that takes in a response writer and a request. So now we can return function like so. For both of our handlers. And then we need to return func. There we go. You could write them like this. And then, like, because we're in the create user and the getUser the way we have right now, we need to inject some dependencies in terms of our database connection. So this is what you could do here. Say, sql.db. Same thing right here. Say, dbConnection sql.db. And then we would, when actually applying these in the main.go file, we'll say handlers, getUser, and then the dbConnection. And then the same thing here. Right. So let's just focus on the handlers for now because we are no longer going to be validating and inserting the data directly. We have our models to handle that. So we will still have this passUserRequest function in the handler because this is related to HTTP and the data is coming in and out. But the actual logic for validating and inserting the user in a database is now in our models. Handle error. As before, we're going to ignore the error here. Same thing down here. Our models will include the getUser, the connection, and then some ID. And this one should now return the user. Actually, it shouldn't return the user. It should return the views.userResponse. That is an ID that would come from the user model. We have an error here. What am I doing wrong? This. Yeah. So again, errors, errors all over. This is not a functional example, but we would marshal this into a byte and then return that. So this is now how our handlers will look like. So we can see that we have one more thing we need to move. We need to move this one. And now we have some nice separations and we can actually just remove these. So now our handlers is only focused on taking some incoming data, parsing it, and then parsing it along to the relevant parts of the code. And then we have our models handling business logic and database and our views handling whatever the response should be. And our main.go file literally just becomes this very thin entry point into our application.

Early Access

$95 $65 USD

Get access

Invoices and receipts available for easy company reimbursement