Circular imports break the dependency graph
You are building a Go service. You have a user package that manages user profiles and a permission package that checks access rights. The user package needs to call permission.Check to validate roles. You add the import. The build works. Then you realize permission.Check needs to look up the user's email to verify it against a policy. You add an import of user inside permission. You hit build. The compiler stops immediately.
The error message is blunt: import cycle not allowed: example.com/myapp/user -> example.com/myapp/permission -> example.com/myapp/user.
This is not a bug. Go forbids circular dependencies by design. The language requires the dependency graph to be a directed acyclic graph. If package A imports package B, package B cannot import package A, directly or indirectly. The compiler enforces this rule to guarantee deterministic initialization order and to prevent hidden coupling between packages. When you see this error, the fix is never to convince the compiler to allow the cycle. The fix is to restructure the code so the cycle disappears.
Why Go forbids cycles
Go loads packages in a specific order determined by the import graph. Before any code runs, the compiler resolves all dependencies. It ensures that a package is fully loaded before any package that imports it can execute. This includes running init functions. If package A imports package B, all of B's init functions run before any of A's init functions.
A circular dependency breaks this ordering. If A imports B and B imports A, the compiler cannot decide which package to load first. Loading A requires B to be ready. Loading B requires A to be ready. The result is a deadlock in the initialization phase. Go chooses to make this a hard compile-time error rather than guessing an order. Guessing would lead to subtle runtime bugs where init functions rely on state that has not been set up yet.
The rule also forces clean architecture. Circular imports usually signal that two packages share too much responsibility or that a shared type is defined in the wrong place. Breaking the cycle often reveals a better design where concerns are separated clearly.
Minimal example of the error
Consider two packages that depend on each other. Package pkgA defines a type and uses a function from pkgB. Package pkgB uses the type from pkgA.
// pkgA/a.go
package pkgA
import "example.com/myapp/pkgB"
// Item represents a data item.
type Item struct {
Value int
}
// Process uses a helper from pkgB.
func Process() {
pkgB.Analyze(Item{Value: 10})
}
// pkgB/b.go
package pkgB
import "example.com/myapp/pkgA"
// Analyze checks an item from pkgA.
func Analyze(item pkgA.Item) {
_ = item.Value
}
The compiler rejects this program with import cycle not allowed: example.com/myapp/pkgA -> example.com/myapp/pkgB -> example.com/myapp/pkgA. The error lists the chain of imports that form the loop. The build fails before any object code is generated.
Realistic scenario: model and service
A common source of cycles is the separation between data models and business logic. You might have a model package with structs and a service package with functions. The service package imports model to work with the data. The cycle appears when model needs logic from service.
Perhaps you want a method on the User struct that validates itself using complex rules defined in service. Or maybe model wants to use a repository interface defined in service. Both cases create a cycle.
// model/user.go
package model
import "example.com/myapp/service"
// User holds user data.
type User struct {
Name string
Role string
}
// IsValid delegates validation to the service layer.
// This creates a cycle because model imports service.
func (u User) IsValid() bool {
return service.ValidateUser(u)
}
// service/validate.go
package service
import "example.com/myapp/model"
// ValidateUser checks if a user meets requirements.
func ValidateUser(u model.User) bool {
return u.Name != "" && u.Role != ""
}
The compiler stops with import cycle not allowed. The model package should not know about service. Data structures are cheap to pass around. Logic should depend on data, not the other way around. The fix depends on what is shared.
Fix 1: Extract shared types
The most common fix is to move shared types into a third package that both model and service can import. This package contains only type definitions, constants, and perhaps simple validation helpers. It contains no business logic and imports nothing from the application layer.
Create a types or shared package. Move the User struct there. Update model and service to import the shared package.
// types/user.go
package types
// User holds user data shared across packages.
type User struct {
Name string
Role string
}
// model/user.go
package model
import "example.com/myapp/types"
// User wraps the shared type with model-specific behavior.
type User struct {
types.User
}
// NewUser creates a user with default values.
func NewUser(name string) User {
return User{User: types.User{Name: name, Role: "viewer"}}
}
// service/validate.go
package service
import "example.com/myapp/types"
// ValidateUser checks if a user meets requirements.
func ValidateUser(u types.User) bool {
return u.Name != "" && u.Role != ""
}
Now model and service both import types. Neither imports the other. The cycle is broken. The types package is a leaf in the dependency graph. It has no dependencies on the rest of the application.
Convention aside: Receiver names in Go are usually one or two letters matching the type. Use (u User) or (n Node), not (this User) or (self User). This keeps code concise and matches the standard library style.
Shared packages should be boring. They hold types and constants. They do not hold logic. If you start putting functions in types that import other packages, you are just moving the cycle or creating a god package that couples everything together. Keep shared packages truly shared.
Fix 2: Use interfaces to invert dependency
When one package needs behavior from another, you can break the cycle by defining an interface in the importing package. The package that needs the behavior defines the interface. The package that provides the behavior implements it. The provider imports the consumer to see the interface. The consumer does not import the provider.
This pattern follows the Go convention of "accept interfaces, return structs." The consumer accepts an interface. The provider returns a struct that implements the interface.
// model/user.go
package model
// Validator defines behavior for checking user validity.
// model defines the interface it needs, avoiding an import of service.
type Validator interface {
Validate(u User) bool
}
// User holds user data.
type User struct {
Name string
Role string
}
// Check uses a validator without knowing the implementation.
func (u User) Check(v Validator) bool {
return v.Validate(u)
}
// service/validate.go
package service
import "example.com/myapp/model"
// RealValidator implements model.Validator.
type RealValidator struct{}
// Validate checks user requirements.
func (v RealValidator) Validate(u model.User) bool {
return u.Name != "" && u.Role != ""
}
// Run demonstrates usage.
func Run() {
var v model.Validator = RealValidator{}
user := model.User{Name: "Alice", Role: "admin"}
_ = user.Check(v)
}
Here model defines Validator. service imports model to implement the interface. model does not import service. The dependency flows one way. service depends on model. The cycle is broken.
This approach is powerful for testing. You can pass a mock validator to model without importing the real service. It also keeps model lightweight. It only knows about the interface, not the implementation details.
Fix 3: Pass callbacks for simple actions
If the dependency is a single function call, you can pass a callback function instead of importing the package. This works well when the logic is simple and you don't need a full interface.
// pkgA/a.go
package pkgA
// Fetcher is a function type for retrieving data.
type Fetcher func() string
// DoWork calls the fetcher and processes the result.
func DoWork(f Fetcher) {
data := f()
// process data
_ = data
}
// pkgB/b.go
package pkgB
import "example.com/myapp/pkgA"
// GetSecret returns a secret string.
func GetSecret() string {
return "secret"
}
// Main uses the callback.
func Main() {
pkgA.DoWork(GetSecret)
}
pkgA does not import pkgB. It accepts a function. pkgB imports pkgA and passes its function. The cycle is avoided. This is useful for small helpers or configuration lookups. For complex dependencies, prefer interfaces or dependency injection.
Fix 4: Dependency injection
When a package needs multiple dependencies, dependency injection breaks cycles by passing dependencies through a constructor or a context. The package that needs the dependencies accepts them as parameters. The caller assembles the dependencies and passes them in.
// service/handler.go
package service
import "example.com/myapp/model"
// Handler depends on a validator and a repository.
type Handler struct {
validator model.Validator
repo model.Repository
}
// NewHandler creates a handler with injected dependencies.
func NewHandler(v model.Validator, r model.Repository) *Handler {
return &Handler{validator: v, repo: r}
}
// Handle processes a request.
func (h *Handler) Handle(u model.User) bool {
if !h.validator.Validate(u) {
return false
}
return h.repo.Save(u)
}
The service package imports model for the interfaces. The model package does not import service. The cycle is broken. The caller, often a main package or a setup function, wires everything together. This keeps packages decoupled and testable.
Pitfalls and compiler errors
The compiler error is always import cycle not allowed. It lists the full chain of imports. If the cycle is long, the error helps you trace the path. Look for the package that imports back to an earlier package in the chain. That is where the cycle closes.
Common pitfalls include:
- Creating a
sharedpackage that imports everything. This turnssharedinto a god package that couples all modules together. Shared packages should have no imports from the application layer. - Moving logic into shared types. Shared packages should hold data structures and constants. Logic belongs in leaf packages that depend on shared types.
- Overusing interfaces. Interfaces are great for breaking cycles and testing. Defining an interface for every struct adds noise. Use interfaces when you need to swap implementations or break a dependency.
- Ignoring the error and merging packages. Sometimes the right fix is to put the code in a single package. If two packages are tightly coupled and share many types and functions, they might belong together. Splitting them artificially creates cycles. Merge them and refactor later if the package grows too large.
Convention aside: Public names start with a capital letter. Private names start lowercase. Go uses capitalization for visibility, not keywords like public or private. When you extract types to a shared package, ensure the types and fields are exported if other packages need them. Unexported fields are invisible outside the package.
Another convention: context.Context always goes as the first parameter, conventionally named ctx. If your service functions take a context, the signature should be func(ctx context.Context, ...). This helps tools detect cancellation and deadlines. It also keeps signatures consistent.
Decision matrix
Use a shared types package when two packages need the same struct or interface definition.
Use an interface in the importing package when you need to depend on behavior without importing the implementation.
Use a callback function when one package needs to invoke a single action from another without a direct import.
Use dependency injection when a package requires multiple dependencies that are assembled by the caller.
Use a single package when the code is small enough to fit together without splitting.
Merge tightly coupled packages when they share extensive logic and types that cannot be cleanly separated.
Where to go next
- How to Import Packages in Go
- What Is the init Function and Package Initialization Order
- Create and publish module
Cycles break initialization order. Go refuses to guess. Extract types, define interfaces, or inject dependencies. Structure follows data flow.