Introduction
In large Go projects, adopting monorepo architecture can bring many advantages such as code sharing and unified build processes. However, as the project scale grows, dependency management issues become increasingly prominent: different services may need different versions of third-party libraries, dependency version conflicts between shared packages and services, and complex transitive dependency relationships make dependency management tricky.
Based on actual project experience, this article summarizes a set of effective Go monorepo dependency management solutions, successfully solving complex dependency conflict issues through modular dependency management and rational use of replace directives.
Problem Background
In monorepo architecture, multiple services and shared packages (pkg/) may depend on different versions of third-party libraries, leading to:
- Version conflicts: Different services need different versions of the same dependency (such as Kubernetes, certain framework libraries).
- Transitive dependency conflicts: Incompatible versions of indirect dependencies.
- Local dependency management difficulties: Dependency versions of shared packages (
pkg/) are difficult to unify. - High build complexity: A single
go.modis difficult to meet the needs of all submodules.
Solution
1. Modular Dependency Management
Create independent go.mod files for each submodule to achieve dependency isolation:
my-monorepo/
├── go.mod # Root module
├── pkg/
│ ├── go.mod # Shared package module
│ ├── k8s/
│ │ └── go.mod # k8s toolkit module
│ ├── storage/
│ │ └── go.mod # storage toolkit module
│ └── repository/
│ └── go.mod # repository toolkit module
├── cmd/
│ └── cli-tool/
│ └── go.mod # CLI tool module
└── services/
├── service-a/
│ └── go.mod # Service module
├── service-b/
│ └── go.mod
└── service-c/
└── go.mod
Advantages:
- Each module can independently manage dependency versions.
- Avoid unnecessary dependency transmission.
- Clearer dependency boundaries.
- Easy to build and test separately.
2. Use replace directives to manage local dependencies
In go.mod of each submodule, use replace directive to reference dependencies in local paths:
Root module (go.mod)
module my-monorepo
go 1.21
replace (
my-monorepo/pkg => ./pkg
my-monorepo/pkg/storage => ./pkg/storage
my-monorepo/pkg/k8s => ./pkg/k8s
)
Service module example (services/service-a/go.mod)
module my-monorepo/services/service-a
go 1.21
replace (
my-monorepo/pkg => ../../pkg
my-monorepo/pkg/repository => ../../pkg/repository
)
require (
my-monorepo/pkg v0.0.0-00010101000000-000000000000
my-monorepo/pkg/repository v0.0.0-00010101000000-000000000000
// ... other dependencies
)
Key Points:
- Use relative paths (
../../pkg) to reference local modules. - Use pseudo-version
v0.0.0-00010101000000-000000000000as version identifier for local dependencies. - Each module only declares local dependencies it directly uses.
- Relative paths are calculated based on the directory where the module is located.
3. Unify Third-party Dependency Versions
3.1 Unify Kubernetes Versions
The dependency relationship of the Kubernetes ecosystem is complex, and different packages may depend on different versions. Unify versions of Kubernetes-related dependencies in go.mod of shared packages:
module my-monorepo/pkg
go 1.21
replace (
k8s.io/apiextensions-apiserver => k8s.io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20240417172702-7c48c2bd72b9
k8s.io/client-go => k8s.io/client-go v0.30.0
k8s.io/kubernetes => k8s.io/kubernetes v1.30.0
k8s.io/cri-api => k8s.io/cri-api v0.30.0
)
Reason:
- The dependency relationship of the Kubernetes ecosystem is complex, and different packages may depend on different versions.
- Force unified versions through
replaceto avoid version conflicts. - Use packages in the staging directory to ensure version consistency.
3.2 Third-party Dependency Replacement
When using fork versions or internal versions, use replace in relevant modules:
// Replace with internal fork version
replace (
github.com/example/library/v2 => github.com/my-org/library/v2 v2.1.0-custom
)
Application Scenarios:
- Need to use internal fork of third-party libraries.
- Need to fix bugs but upstream has not merged yet.
- Need to use specific versions of dependencies.
3.3 Dependency Version Selection Principles
- Unify shared dependency versions: Unify management of shared dependency versions in
pkg/go.mod. - Independent management of service-specific dependencies: Each service can have its own dependency versions.
- Use replace to solve conflicts: When version conflicts occur, prioritize using
replaceto unify versions.
4. Version Strategy
4.1 Go Version
It is recommended to unify all modules to the same Go version to avoid compatibility issues:
go 1.21 // All modules use the same version
If different versions need to be supported due to historical reasons, it is suggested to:
- Gradually migrate to a unified version.
- Clearly state version requirements in documentation.
- Verify compatibility of different versions in CI/CD.
4.2 Dependency Version Management
- Major version dependency: Follow semantic version control, pay special attention to major version changes.
- Test version: Avoid using test versions like
-alpha,-betain production environment. - Security update: Regularly check for security vulnerabilities in dependencies and update in time.
Dependency Management Best Practices
1. Add New Dependency
Add dependency in shared package (pkg/)
cd pkg
go get github.com/example/[email protected]
go mod tidy
Add dependency in service
cd services/my-service
go get github.com/example/[email protected]
go mod tidy
2. Update Dependency Version
Update shared dependency
- Update version in
pkg/go.mod. - Run
go mod tidyto update all submodules. - Check for version conflicts, use
replaceto unify versions if necessary. - Run tests to ensure compatibility.
Update service-specific dependency
- Update in
go.modof corresponding service. - Run
go mod tidy. - Run tests for that service.
3. Handle Version Conflicts
When version conflicts occur:
- Identify conflict source:
bash
go mod why github.com/example/package
go mod graph | grep github.com/example/package
- Unify version: Use
replaceingo.modof relevant modules to unify version.
go
replace github.com/example/package => github.com/example/package v1.2.3
- Test verification: Ensure all modules can be built and run normally.
bash go build ./... go test ./...
4. Local Development
Build specific service
cd services/my-service
go build ./...
Build all modules
# In root directory
go work sync # If using go.work
# Or build each module separately
for dir in cmd/* services/*; do
(cd "$dir" && go build ./...)
done
Use Go Workspace (Recommended)
Go 1.18+ introduced workspace feature, which can simplify local development:
# Create workspace
go work init
go work use ./pkg
go work use ./services/service-a
go work use ./cmd/cli-tool
Create go.work file:
go 1.21
use (
./pkg
./services/service-a
./services/service-b
./cmd/cli-tool
)
5. Docker Build
In Dockerfile, copy and download dependencies first to utilize Docker layer caching for build optimization:
FROM golang:1.21-alpine AS build
WORKDIR /app
# Copy root go.mod
COPY go.mod ./
COPY go.sum ./
# Copy submodule go.mod
COPY cmd/cli-tool/go.mod ./cmd/cli-tool/
COPY cmd/cli-tool/go.sum ./cmd/cli-tool/
COPY pkg/ ./pkg/
# Download dependencies (utilize Docker cache)
RUN cd cmd/cli-tool && go mod download
# Copy source code and build
COPY . .
RUN cd cmd/cli-tool && go build -o /cli-tool
FROM alpine:latest
COPY --from=build /cli-tool /usr/local/bin/cli-tool
ENTRYPOINT ["/usr/local/bin/cli-tool"]
Optimization tips:
- Copy
go.modandgo.sumfirst, utilize Docker layer cache. - Download dependencies before copying source code.
- Use multi-stage build to reduce image size.
Dependency Relationship Diagram Example
Root module (my-monorepo)
├── pkg/
│ ├── Unify Kubernetes v0.30.0
│ ├── Unify shared dependency versions
│ └── Shared toolkit dependency
├── pkg/k8s/
│ └── Kubernetes v0.30.0 (Unified version)
├── pkg/storage/
│ └── Storage related dependency
├── pkg/repository/
│ ├── Database operation dependency
│ └── Git operation dependency
├── cmd/cli-tool/
│ ├── CLI framework dependency
│ └── Depends on pkg and pkg/repository
└── services/
├── service-a/
│ ├── Web framework dependency
│ └── Depends on pkg and pkg/repository
├── service-b/
│ └── Depends on pkg
└── service-c/
└── Depends on pkg and pkg/repository
FAQ
Q1: Why need multiple go.mod files?
A: In monorepo, different services may have different dependency needs. Independent go.mod files allow:
- Independent management of dependency versions for each module.
- Avoiding unnecessary dependency transmission.
- Clearer dependency boundaries.
- Easy to build and deploy separately.
Q2: Will replace directive affect production build?
A: No. replace directive only takes effect during local development. During production build:
- If dependency has been published to repository, Go will use version in repository.
- For local path
replace, same directory structure needs to be maintained in build environment. - It is recommended to verify build in CI/CD to ensure production environment can build normally.
Q3: How to ensure all modules use compatible dependency versions?
A:
- Unify management of shared dependencies in
pkg/go.mod. - Regularly run
go mod tidyandgo mod verify. - Use CI/CD to check for dependency conflicts.
- Establish dependency version update process.
- Use tools like
go mod graphto analyze dependency relationships.
Q4: How to handle cross-module dependency updates?
A:
- First update dependency of shared package (
pkg/). - Run
go mod tidyto update all modules depending on that shared package. - Check for version conflicts.
- Update
replacedirective in service'sgo.modif necessary. - Run full test suite.
Q5: When should go.work be used?
A: go.work is suitable for:
- Local development environment.
- Need to modify multiple modules simultaneously.
- Rapid iteration and testing.
Not suitable for:
- Production build (should use standard
go.mod). - CI/CD environment (unless explicitly required).
Tools and Automation
1. Dependency Check Script
#!/bin/bash
# check-deps.sh
echo "Checking for dependency conflicts..."
for dir in cmd/* services/* pkg/*/; do
if [ -f "$dir/go.mod" ]; then
echo "Checking $dir"
(cd "$dir" && go mod verify && go mod tidy -check)
fi
done
2. Unified Update Script
#!/bin/bash
# update-deps.sh
PACKAGE=$1
VERSION=$2
if [ -z "$PACKAGE" ] || [ -z "$VERSION" ]; then
echo "Usage: $0 <package> <version>"
exit 1
fi
for dir in cmd/* services/* pkg/*/; do
if [ -f "$dir/go.mod" ] && grep -q "$PACKAGE" "$dir/go.mod"; then
echo "Updating $PACKAGE to $VERSION in $dir"
(cd "$dir" && go get "$PACKAGE@$VERSION" && go mod tidy)
fi
done
3. CI/CD Check
Add dependency check step in CI/CD:
# .github/workflows/ci.yml
- name: Verify dependencies
run: |
for dir in cmd/* services/* pkg/*/; do
if [ -f "$dir/go.mod" ]; then
cd "$dir"
go mod verify
go mod tidy -check
fi
done
Future Improvement Directions
- Unify Go Version: Unify all modules to the same Go version.
- Dependency Version Audit: Regularly audit and update dependency versions to ensure security.
- Automation Tools: Develop scripts to automatically synchronize dependency versions.
- Document Dependency Strategy: Clarify which dependencies need unified versions and which can be managed independently.
- Consider using go.work: Fully utilize Go 1.18+ workspace feature for local development.
- Dependency Security Scan: Integrate tools like
govulncheckfor security scanning.
Related Resources
- Go Modules Official Documentation
- Go Modules replace Directive
- Go Workspace Documentation
- Kubernetes Dependency Management Best Practices
Summary
By creating independent go.mod files for submodules and using replace directive to unify management of dependency versions, we can effectively solve complex dependency conflict issues in monorepo. Advantages of this solution include:
- Dependency Isolation: Each module can manage dependencies independently.
- Version Unification: Unify versions of critical dependencies through
replace. - Flexibility: Different services can use different dependency versions (under compatibility premise).
- Maintainability: Clear dependency relationships, easy to maintain and update.
- Extensibility: Easy to add new modules and services.
During implementation, should:
- Follow established dependency management strategies.
- Consider impact on existing modules when adding new dependencies.
- Regularly update and audit dependency versions.
- Keep dependency management documentation updated.
- Automate dependency check in CI/CD.
Hope these practical experiences can help you better manage dependencies in Go monorepo projects and avoid common dependency conflict issues.