Fix

"import cycle not allowed" in Go

Fix Go import cycles by moving shared code to a new package that both conflicting packages can import.

When packages depend on each other

You are building a service. You split your code into packages to keep things tidy. You create a user package for data models and an auth package for login logic. The user package needs to check if a user is active, so it imports auth. The auth package needs to look up a user by ID, so it imports user. You run go build and the compiler stops you dead.

import cycle not allowed

The build fails. You stare at the screen wondering why Go won't let you do what seems like a simple dependency. The error message points to a loop in your imports. You have created a circular dependency. The compiler refuses to resolve it.

The compiler needs a direction

Go compiles code by resolving dependencies in a strict order. Before the compiler can process package A, it must fully understand package B if A imports B. If B also imports A, the compiler hits a wall. It cannot build A without B, and it cannot build B without A. This is a circular dependency. The compiler refuses to guess. It enforces a directed acyclic graph of imports. Cycles break the build graph.

Think of a recipe. Recipe A requires Ingredient B. Recipe B requires Ingredient A. You cannot make either recipe. You need a base ingredient C that both can use. Go requires that base ingredient. The import graph must flow in one direction. Packages can depend on other packages, but those dependencies cannot loop back.

This rule is not arbitrary. It keeps packages independent. It allows the compiler to build packages in parallel. If cycles were allowed, a change in package A could ripple through package B and back to A, forcing a full rebuild of the entire project. Go avoids that cost. It forces you to think about boundaries. If two packages import each other, they are really one package. The compiler is telling you to refactor.

Minimal reproduction

The error appears as soon as the cycle exists. You do not need to call the functions. The mere presence of the imports triggers the check.

// pkgA/a.go
package pkgA

import "example.com/app/pkgB"

// DoA performs work that depends on pkgB.
func DoA() {
    // This import creates a dependency on pkgB.
    pkgB.DoB()
}
// pkgB/b.go
package pkgB

import "example.com/app/pkgA"

// DoB performs work that depends on pkgA.
func DoB() {
    // This import creates a dependency on pkgA.
    // The cycle is now complete.
    pkgA.DoA()
}

The compiler rejects this with import cycle not allowed: example.com/app/pkgA -> example.com/app/pkgB -> example.com/app/pkgA. The message shows the path of the cycle. Follow the arrows to find the loop.

Why Go enforces this

Go's compilation model relies on independent packages. Each package compiles to an object file. The linker combines these files into a binary. If packages could import each other, the compiler would have to handle mutual dependencies at link time, which complicates the build system and slows down compilation. By forbidding cycles, Go ensures that every package can be compiled in isolation once its dependencies are ready.

This design also improves code organization. Cycles indicate tight coupling. When package A depends on package B and package B depends on package A, the two packages share state and logic in ways that make them hard to test or reuse separately. Breaking the cycle forces you to extract shared concerns. The result is cleaner architecture.

Breaking the cycle with interfaces

The most common fix involves interfaces. Go uses structural typing. Interfaces are satisfied implicitly. You do not declare that a struct implements an interface. You just provide the methods. This design is what makes breaking cycles possible. The package that defines the interface holds the dependency. The package that implements the interface can import the interface package without creating a loop.

Consider a realistic scenario. You have a permission package that checks access rules. You have a user package that defines the User struct. The permission package needs to check the user's role. The user package wants to expose a CanAccess method that uses the permission logic.

If permission imports user to get the User type, and user imports permission to call the check function, you have a cycle. The fix is to define an interface in permission. The permission package depends on the interface, not the concrete type. The user package implements the interface and imports permission. The dependency flows one way.

// permission/permission.go
package permission

// Subject defines the requirements for permission checks.
// This interface allows permission logic to depend on abstractions,
// not concrete types from other packages.
type Subject interface {
    GetRole() string
    GetID() string
}

// Check validates access based on the subject's role.
// The function accepts the interface, breaking the dependency on user.User.
func Check(s Subject, resource string) bool {
    return s.GetRole() == "admin"
}
// user/user.go
package user

import "example.com/app/permission"

// User represents an application user.
type User struct {
    ID   string
    Role string
}

// GetRole returns the user's role.
// This method satisfies the permission.Subject interface implicitly.
func (u User) GetRole() string {
    return u.Role
}

// GetID returns the user's ID.
func (u User) GetID() string {
    return u.ID
}

// CanAccess checks permissions using the permission package.
// user imports permission, but permission does not import user.
func (u User) CanAccess(resource string) bool {
    return permission.Check(u, resource)
}

The cycle is broken. permission defines Subject. user implements Subject by providing GetRole and GetID. user imports permission to call Check. permission does not import user. The dependency graph is acyclic.

This pattern follows the Go convention: accept interfaces, return structs. The permission package accepts the Subject interface. The user package returns the User struct. The interface lives in the package that uses it. The implementation lives in the package that defines the data.

Other strategies and pitfalls

Interfaces are not the only fix. Sometimes the right move is to extract shared code into a third package. If pkgA and pkgB both need a common type or utility function, move that code to pkgC. Both pkgA and pkgB can import pkgC without creating a cycle.

// types/types.go
package types

// User represents a shared data model.
// Both user and permission packages can import this package.
type User struct {
    ID   string
    Role string
}
// permission/permission.go
package permission

import "example.com/app/types"

// Check validates access for a user.
// permission imports types, not user.
func Check(u types.User, resource string) bool {
    return u.Role == "admin"
}
// user/user.go
package user

import "example.com/app/types"

// GetUser returns a user.
// user imports types, not permission.
func GetUser(id string) types.User {
    return types.User{ID: id, Role: "admin"}
}

This works, but watch out for the common trap. If you create a shared or common package and dump everything there, it becomes a dependency for every package. You trade import cycles for a god package that couples everything together. Extract only what is truly shared. Keep the third package focused.

Another pitfall is indirect cycles. The error message shows the full path. import cycle not allowed: pkgA -> pkgB -> pkgC -> pkgA. You might only see A and B importing each other, but the cycle could involve three or more packages. Follow the path in the error message. Do not assume the cycle is direct.

Merging packages is also a valid fix. If pkgA and pkgB are so tightly coupled that splitting them causes constant cycles, they might belong in a single package. Go favors small, cohesive packages. If two packages share so much logic that they import each other, combining them reduces complexity. The compiler is not trying to be difficult. It is pointing out a design flaw. Sometimes the flaw is that you split too much.

The main package is a special case. Nothing imports main. It is the root of the dependency graph. You cannot create a cycle involving main because no other package can import it. If you put logic in main and then try to reuse it elsewhere, you will hit import errors. Move reusable logic out of main into other packages. Keep main as the entry point only.

Decision: how to restructure

Use an interface when package A needs to pass data to package B, and B only cares about specific methods. Define the interface in B. Implement it in A. B depends on the interface, not A.

Use a shared types package when multiple packages reference the same structs or constants. Extract the types into a new package that both can import. Keep the shared package small and focused.

Use a third package when logic is shared between A and B but doesn't fit cleanly in either. Move the shared logic to C. Have A and B import C. Avoid creating a dumping ground for unrelated code.

Merge packages when A and B are so tightly coupled that splitting them causes constant cycles. Combine them into a single package. Small packages are good. Circular packages are impossible.

Where to go next