By Matt King for the GopherCon Liveblog on August 28, 2018
Presenter: Kat Zień
Liveblogger: Matt King
(Photo credit: arbourdx)
How should I structure my Go code?” is probably one of the most commonly asked questions, by new and experienced programmers alike. There is almost always more than one answer and it can be tricky to decide what will work best.
Should I keep all my files under one directory or should I split them up? How should I divide my code and into what packages? Can I write object-oriented code in Go? Why do some projects have a cmd directory and what is the advantage of that?
To answer the question, “How should I structure my Go code?” Kat first provides an overview of common design patterns such as:
Throughout the talk, Kat clearly demonstrates the importance of having consistent structure that is simple, testable, and easy to understand. This leads into a discussion and example project using the design driven development approach.
One of the most important principals of the design driven development approach is that the structure of the project should reflect how the software works.
The demo app is a real world example of a high quality, clear, and, maintainable project structure.
Kat answers the opening question by building a beer reviewing service that has a simple, but realistic set of requirements.
There's a variety of ways one can dive in and start organizing a project. Some common ways of the beer reviewing service could be structured are:
As the application gets more complex we can try other approaches:
However, there are some issues with the group by function approach. For example, it could be problematic if a variable needs to be shared by two different layers, which layer do you put it in? Or do you duplicate? Or it could be tough to understand where does initialization go? Does main initialize a storage shared between models, or does each model initialize their own storage.
There's a different approach that we can try - Grouping by context or also known as Domain Driven Development.
Domain driven development works by first establishing your domain and business logic. It makes you think about the domain you’re dealing with and the business logic in your app before you even write a single line of code. Next, you’ll define bounded contexts which help you decide what has to be consistent and what can develop independently.
We can use the demo project to better understand each of the building blocks of the system:
Context: beer tasting.
Language: beer, review, storage
Entities: Beer, review
Value Objects: Brewery, Author
Events: Beer added, review added, beer already exists, beer not found.
Repository: Beer repository, review repository
These building blocks result in a new type of structure: Group by Context
In the real world you could be dealing with this type of complexity so this is a perfect example that takes a very academic concept and makes a fully functional application out of it.
Hexagonal architecture helps achieve the goal of being able to easily change one part of the application without having to rewrite the entire thing. The key rule in the hex model is that dependencies are only allowed to point inwards.
The outer layer in the hex model can reach out to the domain as much as they like, but the domain cannot reference anything outside of it. This implies a heavy use of interfaces and Inversion of Control, and we define interfaces at every boundary to abstract away the details for each layer.
In the demo application we end up with the following architecture:
A huge benefit of this structure that becomes more visible in larger projects, is how it is very easy to extend it’s functionality without affecting multiple parts of the codebase. For example, you want to create an RPC version of the API. You would add it to the
pkg directory and implement the interfaces.
The convention seems to be putting your binaries in a separate top-level
cmd directory. If your project is large and has non-Go assets, use a
pkg/subdirectory to separate your Go code. It’s also favorable to group by context instead of functionality to the issues mentioned in other design patterns.
Although there is no single success formula, it’s helpful to know all of the possible approaches and practice implementing the various architectures to become more comfortable.