Have you ever tried to clean up dead code in a Go monorepo with multiple services? Running deadcode on each service separately gives you conflicting results, and you can't safely delete anything. But there's a better way.
🇨🇿 V češtině si lze článek přečíst na kutac.cz
We maintain Go monorepo where multiple services share internal packages. Over time, code accumulates, and you want to clean up unused functions. But how do you know what's truly unused when you have about 10 services importing all different pacakges?
There are linters unused in GolangCI or unsedfunc in GoPLS. However those can detect only unexported unused functions within package. Go team also did a great tool called deadcode. But it accept single entrypoint and checks for unreachable code from single main function.
The problem with Simple Intersection
You might think: "Just run deadcode on each service and delete functions reported as dead in ALL services." This fails when services don't import the same packages.
Consider this monorepo structure:
├── services/
│ ├── service-a/ (imports pkg/cache, pkg/logging)
│ ├── service-b/ (imports pkg/cache, pkg/logging)
│ └── service-c/ (imports pkg/logging only)
└── pkg/
├── cache/
│ ├── Get() ✓ Used by service-a
│ ├── Set() ✓ Used by service-b
│ └── Delete() ✗ Actually dead
└── logging/
└── Info()
Running deadcode on each service:
service-a: Reports cache.Set(), cache.Delete() as dead
service-b: Reports cache.Get(), cache.Delete() as dead
service-c: Reports NOTHING about pkg/cache (doesn't import it!)
Simple intersection (dead in ALL services): EMPTY SET ❌
Why? service-c doesn't import pkg/cache at all, so it doesn't report anything about it. The intersection finds nothing, even though cache.Delete() is genuinely unused by all services!
The Solution: Package-Based Intersection
I created deadmono to solve this with package-based intersection:
- Track which packages each service imports
- Analyze each entrypoint separately
- Intersect results per package (only among services that import it)
- Report truly dead functions
Installation
# Install deadmono
go install github.com/arxeiss/deadmono/cmd/deadmono@latest
# Install the required deadcode tool
go install golang.org/x/tools/cmd/deadcode@latest
Usage
deadmono services/authn/main.go services/config/main.go services/healthcheck/main.go
This analyzes all three services and reports functions unused by all of them.
How It Works on the Example Above
For pkg/cache (imported by service-a, service-b):
service-a reports: cache.Set(), cache.Delete() as dead
service-b reports: cache.Get(), cache.Delete() as dead
service-c: IGNORED (doesn't import pkg/cache)
Intersection (service-a ∩ service-b):
✓ cache.Delete() - dead in BOTH services that import it
For pkg/logging (imported by all services):
All services use logging.Info()
Intersection: (empty - no dead functions)
Final Result:
pkg/cache/cache.go:15:1: unreachable func: Delete
Now you can confidently delete cache.Delete() knowing it's truly unused.
Useful Flags
-
-test- Analyze test executables too -
-generated- Include dead functions in generated files -
-tags string- Comma-separated list of build tags -
-filter string- Filter packages by regex (default: current module) -
-json- Output in JSON format -
-debug- Verbose debug output
Multiple Go Modules
By default, all entrypoints must be in the same module. To analyze across modules, you must provide -filter flag:
deadmono -filter "github.com/myorg/.*" \
module1/services/api/main.go \
module2/services/worker/main.go
If you maintain Go monorepos, give deadmono a try. It's open source and contributions are welcome!
Top comments (0)