Monorepo with Go: Organizing Code in Practice
\n\nA monorepo is an approach to source code management where all projects, libraries, and services are stored in a single repository. In the Go world, this approach has become especially popular due to built-in module support and a flexible package system. In this article, we will break down how to organize a monorepo in Go in practice, examine a typical structure, code examples, and best practices.
\n\nWhy Do You Need a Monorepo in Go?
\n\nA monorepo solves several key problems:
\n- \n
- Simplifying dependency management — all internal packages are in one place, making them easy to update and refactor. \n
- Unified change history — atomic commits that affect multiple services at once work without extra synchronization. \n
- Faster CI/CD — you can set up pipelines that build only the changed parts of the project. \n
- Improved code quality — it's easier to enforce uniform standards, linters, and code reviews. \n
Go Monorepo Structure: Basic Layout
\n\nLet's look at a typical Go monorepo structure. For example, let's take a project consisting of two microservices (api and worker) and a shared library lib:
my-monorepo/\n├── go.work\n├── go.work.sum\n├── apps/\n│ ├── api/\n│ │ ├── go.mod\n│ │ ├── main.go\n│ │ └── handler.go\n│ └── worker/\n│ ├── go.mod\n│ ├── main.go\n│ └── process.go\n├── lib/\n│ ├── go.mod\n│ ├── auth/\n│ │ └── auth.go\n│ └── db/\n│ └── db.go\n├── scripts/\n│ ├── build.sh\n│ └── test.sh\n└── Makefile\n\nKey elements:
\n- \n
go.work— the Go Workspace file that links all modules together. \n apps/— the directory for applications (microservices, CLI utilities, etc.). \n lib/— shared libraries used by all applications. \n scripts/— auxiliary scripts for building, testing, and deployment. \n
Setting Up Go Workspace (go.work)
\n\nGo 1.18+ provides the workspace mechanism, which allows you to work with multiple modules simultaneously. Let's create a go.work file in the root:
go 1.22\n\nuse (\n ./apps/api\n ./apps/worker\n ./lib\n)\n\nNow, during development, you can run commands from the project root, and Go will see all modules as a single unit. For example, to run tests for all modules:
\n\ngo test ./...\n\nAnd to build a specific service:
\n\ncd apps/api && go build -o bin/api .\n\nCode Example: Shared Library and Microservice
\n\nLet's create a simple authentication library in lib/auth/auth.go:
package auth\n\nimport "errors"\n\nvar (\n ErrInvalidToken = errors.New("invalid token")\n)\n\n// ValidateToken checks the token and returns the user ID.\nfunc ValidateToken(token string) (string, error) {\n if token == "" {\n return "", ErrInvalidToken\n }\n // Here could be real JWT validation or a call to an external service\n return "user-123", nil\n}\n\nNow let's use this library in the microservice apps/api/handler.go:
package main\n\nimport (\n "encoding/json"\n "net/http"\n "my-monorepo/lib/auth"\n)\n\ntype UserResponse struct {\n UserID string `json:"user_id"`\n}\n\nfunc UserHandler(w http.ResponseWriter, r *http.Request) {\n token := r.Header.Get("Authorization")\n userID, err := auth.ValidateToken(token)\n if err != nil {\n http.Error(w, "Unauthorized", http.StatusUnauthorized)\n return\n }\n\n resp := UserResponse{UserID: user