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:
buildTo build the application binaries.testTo run unit/integration tests.runTo execute an application binary.watchTo run the application with file watching (usually using reflex withgo run).dockerTo 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
appsubfolder, e.g../app/myapp. - Code internally shared by your application lives in
./pkg. - I typically put SQL schema definitions & migrations in a
./dbfolder, and leverage tools likepackrto package them in the application binary. More on that in another post. - I typically put application configuration logic in an
env.goand 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.
mockgenlive in./mocks. - Common code that is only ever required for testing might live in packages inside
./pkg. One example of this issql-mockcustom 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!