How I Build Robust, Scalable Go Applications
How I use interfaces to keep my Go applications modular
Suppose we are creating a back-end application in Go. In this back end, we should be able to store and query users.
In this article, I’ll show how I usually set up such a project.
In Go, I usually create repositories for resources. A repository is responsible for saving and retrieving a resource. In this case, the resource would be a user object.
I create a
userrepo package and create a file,
api.go. The API file will hold all the functions that are exported.
This way, there is one central place where you define how other packages can interact with the
I define the
UserRepository interface. For now, we only have two simple methods, one for storing a user and one for finding a user.
I have defined an interface, now it’s time to implement this interface. Let’s start with a
As the name suggests, this won’t do anything but mock all the functions.
I create a file,
mock.go, in the
mockUserRepo implements the
UserRepository interface, and for each function, it just prints a message.
Let’s go back to the
api.go file and add a
New() function. This will return an implementation of the
Why Use an Interface?
Since we have defined a
UserRepository interface we can have multiple implementations for this.
Way back when I just started with software development, I always scratched my head when looking at interfaces and thought: “So what, I just need one implementation anyway.”
This might be true for small, personal projects, but in larger, real-world applications you usually have multiple implementations.
For example, you could add a
cloudUserRepository that connects to a Google Cloud database.
It would make sense to use this in the production environment. However, in your local environment, it would probably be easier to add an SQLite database.
By creating a
sqliteUserRepository that implements the
UserRepository interface, you can easily accomplish this without having to rewrite a large part of your code.
OK, I’ll show an example to convince you that interfaces really are awesome.
Let’s create an
sqlite.go file in the
userrepo package. This file will contain the SQLite implementation of the
Similarly, let’s create a
cloud.go file in the
userrepo package that will contain the Google Cloud implementation.
Note that, for now, I just print a debug message, implementing the actual interfaces is out of scope for this article.
Next, I update the
We pass the environment to the
New() function and it will return the correct implementation of the
UserRepository based on this environment — Awesome!
Bringing It All Together
Let’s create the main program.
In a new package,
main, I create a
Here, I use the
userRepo.New() function to get the implementation of the
UserRepository based on the environment.
If I ever need to swap user repositories on an environment, I only have to change the
userrepo.New() function, which is awesome.
Let’s test it by running the following command:
go run main/run.go
Which outputs the following:
cloudUserRepo: mocking the StoreUser func
So, the correct implementation of the
UserRepository interface is created.
This is a design pattern that I use a lot and has proven to be very useful. Hopefully, you find the pattern just as awesome as I do!
You can find the repository on GitHub here.