Structuring Projects in Go
Project Layout
The Go community has, by-and-large, agreed on some broad standards for how to structure Go projects based on a set of common historical and emerging project layout patterns. These best practices for project layout are described in detail here.
Here’s how I typically structure a Go microservice, based on these best practices:
.env
go.mod
go.sum
README.md
Makefile
Dockerfile
/bin
/scripts
/cmd
/myapp
main.go
/anotherapp
main.go
/pkg
/internal
env.go
/app
/myapp
index.go
/anotherapp
index.go
/pkg
/db
/test
/testdata
/mocks
/pkg
.env
I highly recommend using an env file to set your service environment variables locally (just make sure you’ve added it to your .gitignore
so that it’s not checked into version control). It has the format:
VAR1=value1
VAR2=value2
I use the following bash snippet to load the environment in my current shell session before starting the service. This script usually lives in ./scripts/env.sh
and can be sourced from the Makefile
(more on that below). Note that this snippet supports commenting out lines in the .env
using a #
character.
#!/bin/bash
if [ -f "$1" ]; then
export $(egrep -v '^#' $1 | xargs)
fi
Makefile
The ubiquitous Makefile has been here since the dinosaurs roamed the earth. Best to keep it as simple as possible. If your targets start growing too large, stick them in a bash script and call that from your target instead.
At minimum, I typically implement some form of the following targets:
build
To build the application binaries.test
To run unit/integration tests.run
To execute an application binary.watch
To run the application with file watching (usually using reflex withgo run
).docker
To build a container image.
For commands that require it, I typically load the environment by sourcing the env script before execution:
env=.env
.PHONY: run
run: build
@source ./scripts/env.sh $(env) && ./bin/myapp
Then make run
will execute the application binary with the environment loaded from .env
. The env file can be overridden with e.g. make run env=.env2
.
I’ll include more Makefile tips and tricks in a later post.
/cmd
This structure gives you the flexibility to create multiple application entrypoints, building separate binaries for each of them. For example, given a build
Make target like:
cmd=myapp
.PHONY: build
build:
@go build -o ./bin/$(cmd) ./cmd/$(cmd)/main.go
…you can then build the default myapp
binary with make build
, or the anotherapp
binary with make build cmd=anotherapp
.
In addition to working well in a monorepo context, this pattern can also allow you to build lightweight binaries for specific tasks, without having to build the larger codebase (as would be required if using subcommands in the main application binary like ./bin/myapp subcommand
).
/pkg
Mimicking the $GOPATH
package folder, code that lives in here is public. It is intended for reuse by external projects. You are exposing an API that may be depended on by others and as such have a responsability to correctly follow semantic versioning.
/internal
This is code that is for internal use by your application only. The bulk of your application logic will reside here. The subfolder structure inside /internal
is similar to that of the top level.
- The application entrypoint(s) typically live in the
app
subfolder, e.g../app/myapp
. - Code internally shared by your application lives in
./pkg
. - I typically put SQL schema definitions & migrations in a
./db
folder, and leverage tools likepackr
to package them in the application binary. More on that in another post. - I typically put application configuration logic in an
env.go
and leverage Viper. More on that here.
/test
Don’t forget this folder.
Testing is a broad and important topic, which I’ll certainly cover in future posts. For now, a high-level description will suffice:
- Fixtures and ground-truth live in
./testdata
. - Mocks, generated with e.g.
mockgen
live in./mocks
. - Common code that is only ever required for testing might live in packages inside
./pkg
. One example of this issql-mock
custom matching logic.
Note that test files themselves do not typically live here (with the possible exception of end-to-end tests). Instead, test files are colocated with the code under test using the _test
suffix in the filename, as described in the Go documentation.
Conclusion
Application structure is important. A good structure makes it easier for developers to find their way around unfamiliar code. Following established conventions is perhaps most important for this very reason.
That said, many smaller projects do not require this level of organisation. Start off simple, and add structure as your code base grows.
How do you structure your Go apps? Hit me up on Twitter!