TL;DR
Go won't compile when two or more packages import each other โ directly or through a chain. The fix is always structural: pull shared code into a third package that neither conflicting package touches, or merge the packages if they're too tangled to live apart.
What the error looks like
$ go build ./...
package myapp/service
imports myapp/repository
imports myapp/service: import cycle not allowed
Go prints the full import chain so you can pinpoint where the cycle closes. Read it bottom-up: the last package listed completes the circle by importing the first one.
Why Go forbids import cycles
Go compiles packages in strict dependency order. If package A needs package B and B needs A, the compiler hits a deadlock at the type-checking stage โ there's no valid compilation order.
Languages like Java or Python sidestep this at link time. Go doesn't. It's a hard compile-time error with no language-level escape hatch. You fix the structure, full stop.
Detecting the cycle
The error message already shows the chain. For larger projects with tangled graphs, a few extra tools help:
# Show all dependencies of a package
go list -f '{{ .Imports }}' ./internal/service
# Find all cycles across the module
go build ./... 2>&1 | grep 'import cycle'
# Generate a visual dependency graph (requires Graphviz)
go install github.com/kisielk/godepgraph@latest
godepgraph -s ./... | dot -Tpng -o deps.png
Common scenarios and fixes
Scenario 1: Service and repository importing each other
Nine out of ten Go cycle errors look like this. The service layer calls the repository โ fine. The repository then references a type defined in the service package โ not fine.
// BAD: myapp/repository/user.go
import "myapp/service" // cycle!
func (r *UserRepo) Save(u service.User) error { ... }
Fix: Move shared types like User into a dedicated models or domain package. Neither service nor repository imports the other โ both import models.
// myapp/models/user.go โ leaf package, no internal imports
package models
type User struct {
ID int
Email string
}
// myapp/repository/user.go
import "myapp/models" // OK
func (r *UserRepo) Save(u models.User) error { ... }
// myapp/service/user.go
import (
"myapp/models" // OK
"myapp/repository" // OK
)
func CreateUser(repo *repository.UserRepo, email string) error {
u := models.User{Email: email}
return repo.Save(u)
}
Scenario 2: A utility package that reaches into application logic
Someone adds a helper to utils that needs a type from config. Works fine โ until config imports utils for something else.
// myapp/utils/helpers.go
import "myapp/config" // cycle if config imports utils
Fix: Utility packages should have zero internal imports. Pass values in as parameters instead of reaching out to the package that owns them.
// BEFORE โ helper pulls config itself
func GetTimeout() time.Duration {
cfg := config.Load()
return cfg.Timeout
}
// AFTER โ caller provides the value
func FormatDuration(d time.Duration) string {
return d.String()
}
Scenario 3: Two packages that should be one
Sometimes the cycle is a symptom, not the problem. If packageA and packageB call each other constantly and there's no clean extraction point, they belong in the same package.
# Before โ two packages that can't live without each other
myapp/auth/
myapp/session/
# After โ merge into one cohesive package
myapp/auth/ # contains both auth and session logic
Scenario 4: Using an interface to invert the dependency
When you genuinely need bidirectional communication, define an interface in the lower-level package. The upper-level package implements it. No shared import needed โ the dependency flows one way.
// myapp/repository/notifier.go
package repository
// Notifier is implemented by the service layer.
// repository never imports service.
type Notifier interface {
OnUserSaved(id int)
}
type UserRepo struct {
notifier Notifier
}
func NewUserRepo(n Notifier) *UserRepo {
return &UserRepo{notifier: n}
}
func (r *UserRepo) Save(u User) error {
// ... save logic ...
r.notifier.OnUserSaved(u.ID)
return nil
}
// myapp/service/user.go โ implements repository.Notifier
package service
import "myapp/repository"
type UserService struct{}
func (s *UserService) OnUserSaved(id int) {
// handle post-save notification
}
func SetupRepo(svc *UserService) *repository.UserRepo {
return repository.NewUserRepo(svc) // service passed as the interface
}
Structural rules that prevent cycles
- Dependencies flow inward:
cmdโserviceโrepositoryโmodels. Lower layers never import higher layers. Draw this arrow on a whiteboard before you start โ violations become obvious. - Shared types live in leaf packages:
models,domain, ortypesโ packages with zero internal imports. Every other package can safely import them without risk. - Interfaces belong to the consumer: Define an interface in the package that uses it, not the one that implements it. This is idiomatic Go and naturally breaks potential cycles before they form.
- Watch the
internal/commontrap: Acommonpackage that accumulates unrelated helpers solves cycles mechanically but creates a maintenance mess. Be intentional about what actually belongs there.
Verifying the fix
# Clean exit, no output = cycle is gone
go build ./...
# Make sure nothing broke
go test ./...
# Double-check with vet
go vet ./...
# Optional: regenerate the graph to see the before/after
godepgraph -s ./... | dot -Tpng -o deps-after.png
go build ./... exiting with code 0 and silence is your confirmation. Noisy output means there's still work to do.
Quick checklist when you hit this error
- Read the full cycle chain โ identify which package is the odd one out.
- Is the import needed for just one type or function? Move that type to a shared leaf package.
- Do the two packages constantly call each other? Merge them.
- Can you replace the import with a locally defined interface? Do that.
- Never import a higher-layer package from a lower-layer package โ no exceptions.

