There is no right way to structure a Go project.
But there is a best way to have a structured, clear and maintainable Go project structure.
After seeing so many wrong-doings in the file structure of a Go project, making it unmaintainable, I thought it was time to write a post on that.
We will examine what this structure is, why and what to avoid.
The file structure
In the following, we assume your application name is myapp
and the project name is github.com/user/repo
.
The file structure should look as follow:
.
├── .git
├── cmd
| └── myapp
| └── main.go
├── examples
| └── example1
| └── main.go
├── internal
| ├── config
| | └── config.go
| └── store
| └── store.go
├── pkg
| ├── public1
| | └── public1.go
| └── public2
| └── public2.go
├── go.mod
└── go.sum
Dependencies with go.mod
and go.sum
The go.mod
file holds the project name, Go suggested version and dependencies of your Go project.
It usually looks like
module github.com/user/repo
go 1.16
require (
github.com/golang/mock v1.5.0
)
where github.com/golang/mock v1.5.0
is one of our dependencies for example.
The go.sum
should not be modified by hand, and only contains checksums for the dependencies.
The cmd
directory
The cmd
directory contains one directory per application.
In our case, we only have one application myapp
, but it can contain multiple programs, for example:
├── cmd
├── myapp
| └── main.go
└── myotherapp
└── main.go
Each application directory can only contain one file: main.go
Each main.go
file represents the package main
and contains only the main()
function.
Its code should be minimal, the bulk of the code resides in the internal
directory.
📖 The reading of the main.go
file should be quick and provide a solid understanding of what the top moving pieces are.
The internal
directory
Since Go 1.4, the internal directory is treated specially.
It contains code that can be imported by other parts of the application but not imported by other Go projects as a dependency.
We thus use this directory to place packages internal to the project.
For example, in our case we have:
├── internal
├── config
| └── config.go
└── store
└── store.go
The internal/config
and internal/store
directories contain the config
and store
packages respectively.
Each can be imported for example by cmd/myapp/main.go
with:
import (
"github.com/user/repo/internal/config"
"github.com/user/repo/internal/store"
)
But cannot be imported by another Go project, that is outside of github.com/user/repo
.
By default, you should place all your packages in the internal
directory by creating a directory with the name of the package inside.
The pkg
directory
Unlike internal
, the pkg
directory is more of a convention name.
The pkg
directory is meant to contain packages to be imported by other projects.
In our example, we have:
├── pkg
├── public1
| └── public1.go
└── public2
└── public2.go
We have two packages, public1
and public2
.
Each can be imported by any Go project (including this one) with for example
import (
"github.com/user/repo/pkg/public1"
)
⚠️ You should really limit the packages you place in the pkg
directory.
Since these are exported to other projects, you should be careful about semver versioning to avoid breaking other Go projects depending on it.
It’s always good practice to have all your packages in the internal
directory, and only move them to the pkg
directory once they have been proven to:
- Be stable, ideally with full (and deep) unit testing coverage
- Have their exported Go API stable
The examples
directory
The examples
directory should contain runnable examples to showcase your publicly exported Go API from pkg
.
Each example should have a descriptive directory name and a runnable main.go
file with only the func main()
function.
For example:
├── examples
└── example1
└── main.go
Other tips
There are some other tips and best practices that I will go through using bullet points:
Package naming
- Your package name should be the same as the directory containing it, except for the
main
package - Use single words for package names
- Do not use generic names for package names such as
utils
orhelpers
Package nesting
- Try to avoid nesting packages by default
- You can nest packages if you have different implementations for the same interface (e.g. a store interface)
- You can nest packages if you start having a lot of Go files (more than 10) and it really does make sense to make subpackages
Go libraries
If you are writing a Go library with a single purpose, you might want to have your exported public Go API at the top level. For example with this file structure:
.
├── .git
├── examples
| └── example1
| └── main.go
├── internal
| ├── package1
| | └── package1.go
| └── package2
| └── package2.go
├── api.go
├── go.mod
└── go.sum
The differences are as follows:
- no
cmd
directory since this is not a runnable application - no
pkg
directory since this is a library, all exported Go API should be at the top level to reduce the length of import statements api.go
file containing all your Go public API. It should contain your exported interfaces, constants and constructors.
Note that most of your code should still reside in the internal
directory, and you should keep your public Go API to a minimum.
If it really makes sense, you may have directories at the top level to split the import statements, but I would not recommend it.
Conclusion
I hope this gave you a good overview of how and why you should adopt the application and library Go project structure.
If you have any recommendation, feel free to contact me though!
Have a good maintainability! 🌞
Comments