DEV Community

Cover image for Dead code detection in Go monorepos with deadmono
Pavel Kutáč
Pavel Kutáč

Posted on

Dead code detection in Go monorepos with deadmono

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()
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Track which packages each service imports
  2. Analyze each entrypoint separately
  3. Intersect results per package (only among services that import it)
  4. 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
Enter fullscreen mode Exit fullscreen mode

Usage

deadmono services/authn/main.go services/config/main.go services/healthcheck/main.go
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you maintain Go monorepos, give deadmono a try. It's open source and contributions are welcome!

Top comments (0)