Monorepo with Go: Code Organization in Practice
A monorepo is an approach to code management where all projects, libraries, and services are stored in a single repository. In the Go community, this approach is gaining popularity due to its simplicity and efficiency. In this article, we will explore how to organize a Go project in a monorepo in practice: from choosing a structure to setting up CI/CD.
Why a Monorepo for Go?
Go was originally designed as a language for large codebases. Tools like go mod and go work make working with a monorepo natural. The main advantages are:
- Simplified dependencies: all modules use a single version of packages, no version conflicts.
- Unified CI/CD: one configuration file for all services, easier to maintain.
- Refactoring: changing an API in a library immediately affects all consumers in the repository.
- Unified standards: linters, formatters, and code reviews apply to all code.
Monorepo Structure: A Proven Scheme
Let's consider a structure that works well for medium-sized Go projects. Suppose we have two services: auth-service and api-gateway, as well as shared libraries.
my-project/├── apps/ # Executable applications│ ├── auth-service/│ │ ├── cmd/│ │ │ └── main.go│ │ ├── internal/│ │ │ ├── handler/│ │ │ ├── service/│ │ │ └── repository/│ │ ├── go.mod│ │ └── go.sum│ └── api-gateway/│ ├── cmd/│ │ └── main.go│ ├── internal/│ │ ├── middleware/│ │ └── routes/│ ├── go.mod│ └── go.sum├── libs/ # Shared libraries│ ├── config/│ │ ├── config.go│ │ ├── go.mod│ │ └── go.sum│ ├── logger/│ │ ├── logger.go│ │ ├── go.mod│ │ └── go.sum│ └── db/│ ├── postgres.go│ ├── go.mod│ └── go.sum├── scripts/ # Scripts for CI/CD and utilities│ ├── build.sh│ └── test.sh├── go.work # Go Workspace (optional)├── .golangci.yml # Linter configuration├── Makefile # Common commands└── README.mdKey Structure Elements
apps/— all executable applications. Each application has its own entry point (cmd/main.go) and its owngo.mod.libs/— shared libraries. Each library is a separate Go module.scripts/— auxiliary scripts for building, testing, and deployment.go.work— file for Go Workspace, simplifies local development.
Setting Up Go Workspace for Local Development
Go 1.18+ supports Workspace via the go.work file. This allows you to locally reference libraries from libs/ without publishing to a remote repository.
Create a go.work file in the project root:
go 1.22
use ( ./apps/auth-service ./apps/api-gateway ./libs/config ./libs/logger ./libs/db)Now during development, you can import libraries directly:
// apps/auth-service/cmd/main.gopackage main
import ( "my-project/libs/config" "my-project/libs/logger")
func main() { cfg := config.Load() log := logger.New(cfg.LogLevel) log.Info("Auth service started")}Important note: in each application's go.mod, the dependency on the library must point to a local path using replace:
// apps/auth-service/go.modmodule github.com/company/my-project/a