Single Application
A project (and its repository) for a single application is a further development of the guidelines given so far. Almost, if not everything, from the Common and Library sections can, and most likely, should be applied when working on a single service from the layout perspective. There are a couple of recommendations that make the experience better.
One distinction between a service and a library is that there is always at least one artefact, a binary. It also means that a project has at least one main.go file, which is not needed for a library. The experience of working on a project depends, to some extent, on where those files go.
The following will make interaction with a single-application project more convenient and easier:
- use a separate directory for entry points to the service and satellite tools/applications
- use a special, excluded from Git, directory for outputting build artefacts.
Next couple of sections go in expand the suggestions.
Use cmd Directory for Entry Points
Use cmd directory for entry points to your service. Inside the directory, create a directory per each entry point, named after the application. The inner directories should mainly contain only one file, main.go.
Even when a project is a single application, oftentimes it has a number of satellite CLI and development tools, or different modes. Instead of a long list of if statements, this approach allows for small and focused, yet different entry points.
Create a directory named cmd at the top level of the file tree, and move any entry points, i.e. main.go files in to appropriate directories inside cmd. These second-level directories should be named as you want the binaries to be. Each of those directories is an isolated main package, hence a separate entry point to the app, defined in it.
Use bin Directory for Binaries
Use bin directory for build artefacts. The directory must be excluded from the source control system. The directory is used in both development and CI processes.
As you remember, there should be no garbage in a repository. A binary that is built during development process should not appear among committed content.
Usually, a binary is excluded by listing its name in the .gitignore file. While this gives an immediate result and may work, it doesn't mean there is no a better option. This approach is not scalable. If someone builds the service with a custom name for the binary, the output can be accidentally committed. The same with CI, the process will just put files right at the root level.
A way to improve the situation is to have a separate directory in the project which is:
- excluded from Git
- managed (created and cleaned) automatically
- and used by both developers and build tools.
The special directory is named bin, and its place is at the root level. The entire directory is excluded from Git.
Then, since the Common recommendations are in effect, you've got the Makefile. Given the previous tip, the targets work like this:
- inputs are taken from the
cmddirectory, e.g.cmd/my-app - binaries are put to the
bindirectory, e.g.bin/my-app
Combined together, we get:
go build -ldflags "-X main.revision=`git rev-parse --short HEAD`" -o bin/my-app ./cmd/my-app
If the directory doest not exist, make sure it's created as needed. When you do make clean, the target should remove everything from that directory.
All targets in the build or testing pipelines also use this path for outputting and accessing binaries. When running the service locally, you run it from the directory. Nice and tidy: bin/my-app. If typing in extra characters is a concern, define a target the Makefile, and use make run-my-app. Having this as a target is a scalable way since everyone in the team can use it. If need, you can also create a shell alias as alias run-my-app='make run-my-app.
The targets used in CI process also use the bin directory for artefacts, and then packaging and deploy steps seek for the artefacts in there. The process is structured and organised.
Having a separate directory for binaries is important because chances of multiple binaries are very high. One popular scenario is when a developer works under macOS, and uses Docker. While local version is going to be built for the Mac, the Docker version is built for Linux, hence there are two different binaries already.
It's likely that sometimes there will be a need to build binaries for many systems locally. Thus, two entries should be present in the .gitignore file. Then, if a new binary is introduced later, it will be another two entries. That's already a lot to keep track of, and too many files are at the root level.
Instead, define the bin directory once, add it to the .gitignore file, set up all appropriate targets in the Makefile to use the directory, and don't worry about it for the rest of the project's life.
Here is how a single-application repository may look like with the two recent suggestions applied:
├── bin
│ ├── example-agent
│ ├── example-cli
│ ├── example-devsrv
│ └── example-plugins
└── cmd
├── example-agent
│ └── main.go
├── example-cli
│ └── main.go
├── example-devsrv
│ └── main.go
└── example-plugins
└── main.go
As you see, the structure is organised in a good order, and easy to work with.
Even more important the bin and cmd directories are when a project contains code for multiple different services. In this case, it's a monolithic repository, and many people, potentially several teams, are working on it. All previously given recommendations work especially well with a monorepo. That's what we will talk about in the next section.