Question

Understanding Import Cycle Not Allowed in Go

Question

I am getting the Go error import cycle not allowed while trying to test my controller. This is the output:

can't load package: import cycle not allowed
package project/controllers/account
    imports project/controllers/base
    imports project/components/mux
    imports project/controllers/account
import cycle not allowed
package project/controllers/account
    imports project/controllers/base
    imports project/components/mux
    imports project/controllers/account
import cycle not allowed
package project/controllers/account
    imports project/controllers/base
    imports project/components/mux
    imports project/controllers/routes
    imports project/controllers/base

How should I read and understand this error message? Where is the dependency cycle, and how can I identify which import relationship is wrong?

Short Answer

By the end of this page, you will understand what an import cycle is in Go, how to read the chain shown in the error message, why Go rejects circular package dependencies, and how to refactor your packages to remove the cycle.

Concept

In Go, a package can import another package, but the full dependency graph must remain acyclic. That means you cannot have a chain of imports that eventually leads back to the starting package.

A simple example of a cycle is:

// package a imports b
// package b imports a

A longer cycle is also invalid:

// account -> base -> mux -> account

This is exactly what your error is showing.

Why Go disallows import cycles

Go packages are designed to compile cleanly and independently. Circular dependencies make code harder to:

  • compile
  • test
  • understand
  • reuse
  • maintain

If package account depends on base, and base depends on something that eventually depends on account again, then those packages are tightly coupled. Go forces you to break that coupling.

How to read the error

The error shows the import path chain that leads to the cycle.

From your output:

package project/controllers/account
    imports project/controllers/base
    imports project/components/mux
    imports project/controllers/account

This means:

  1. project/controllers/account imports project/controllers/base
  2. project/controllers/base imports project/components/mux
  3. project/components/mux imports project/controllers/account
  4. Now the chain returns to account, so there is a cycle

That creates this loop:

account -> base -> mux -> account

Your second error shows another cycle:

package project/controllers/account
    imports project/controllers/base
    imports project/components/mux
    imports project/controllers/routes
    imports project/controllers/base

That means:

base -> mux -> routes -> base

So you likely have more than one circular dependency in the package graph.

The key idea

The problem is usually not a single bad line in isolation. The problem is the package design: packages in one layer are importing packages from another layer in a way that loops back.

Mental Model

Think of package imports like roads between cities, but traffic is only allowed to move forward.

  • account can go to base
  • base can go to mux
  • but if mux goes back to account, you have driven in a circle

Go does not allow circular road maps between packages.

Another way to think about it: package imports should form a tree or directed graph without loops, not a ring. Once one package depends on another, that dependency direction should stay one-way.

Take Quiz

Syntax and Examples

In Go, imports happen at the package level.

Normal import

package account

import "project/controllers/base"

This is fine if base does not eventually import account again.

Invalid circular structure

// project/controllers/account/account.go
package account

import "project/controllers/base"

func HandleAccount() {
    base.Render()
}
// project/controllers/base/base.go
package base

import "project/components/mux"

func Render() {
    mux.Register()
}
// project/components/mux/mux.go
package mux

import "project/controllers/account"

func Register() {
    account.HandleAccount()
}

This creates:

account -> base -> mux -> account

Go rejects this.

Better structure

A common fix is to move shared logic into a lower-level package that does not depend on controllers.

// project/components/mux/mux.go
package mux

func Register(path string) {
    // routing setup
}
// project/controllers/base/base.go
package base

func Render(name string) string {
    return "rendering " + name
}
// project/controllers/account/account.go
package account

import (
    "project/components/mux"
    "project/controllers/base"
)

func Setup() string {
    mux.Register("/account")
    return base.Render("account")
}

Now the dependencies are one-way:

account -> mux
account -> base

No package points back to account.

Step by Step Execution

Consider this simplified dependency chain:

// account imports base
// base imports mux
// mux imports account

What happens step by step

When Go tries to build or test account:

  1. It starts loading package account
  2. It sees that account imports base
  3. It loads base
  4. It sees that base imports mux
  5. It loads mux
  6. It sees that mux imports account
  7. But account is already in the current import chain
  8. Go detects a loop and stops with import cycle not allowed

Trace of your error

From your message:

account -> base -> mux -> account

This is enough to fail.

And another trace:

base -> mux -> routes -> base

This means even if you fix one cycle, another one may still remain.

Practical way to debug

Open each package in the chain and inspect its imports:

  • project/controllers/account
  • project/controllers/base
  • project/components/mux
  • project/controllers/routes

Write the dependency chain on paper or in comments:

account imports base
base imports mux
mux imports account   <- cycle here

Then check the second one:

base imports mux
mux imports routes
routes imports base   <- cycle here

Real World Use Cases

Import cycle errors often appear in real Go projects when package responsibilities become mixed.

Common situations

  • Controllers and routers importing each other

    • A controller registers routes
    • The router imports controllers to call handlers
    • This easily creates a cycle
  • Base utilities depending on higher-level packages

    • A base package should usually provide common functionality
    • If it imports controller-specific code, it stops being a true base layer
  • Testing setup

    • Test files may import helper packages that import the package under test indirectly
    • This can expose cycles that were already present in the project structure
  • Shared configuration or initialization code

    • A package meant for shared setup sometimes imports feature packages
    • Feature packages then import the shared setup package back

Typical fix direction

Move shared code into neutral packages such as:

  • internal/httpx
  • internal/render
  • internal/router
  • pkg/config
  • pkg/types

These shared packages should not import high-level controller packages.

Take Quiz

Real Codebase Usage

In real Go codebases, developers avoid import cycles by designing clear package layers.

Common package direction

A common structure is:

handlers/controllers -> services -> repositories -> database

Each layer depends only on lower layers, not the other way around.

Useful patterns

1. Dependency inversion with interfaces

Instead of mux importing account, define an interface or register handler functions without importing the controller package back.

package mux

type HandlerFunc func()

func Register(path string, h HandlerFunc) {
    // store route and handler
}
package account

import "project/components/mux"

func Handle() {}

func Setup() {
    mux.Register("/account", Handle)
}

2. Keep shared packages low-level

Packages like base, utils, or common should not import feature packages.

Bad direction:

base -> account

Better direction:

account -> base

3. Separate route registration from route engine

A routing engine package should not need to know about all controllers directly if those controllers can register themselves through exported functions or through a top-level composition package.

4. Composition at the top level

Use main or an app setup package to wire dependencies together.

main imports mux
main imports account
main calls account.RegisterRoutes(mux)

This is often cleaner than having mux import account and account import mux.

5. Guard package boundaries

When a package starts importing from both higher and lower layers, it often becomes a cycle source. Review package roles early.

Common Mistakes

1. Putting shared logic in a package that is not actually shared

A package named base or common often grows too much and starts importing feature packages.

Broken idea:

package base

import "project/controllers/account"

If account already imports base, you now have a cycle.

Avoid it: keep base independent of feature packages.

2. Router and controller importing each other

Broken pattern:

// mux imports account
// account imports mux

Avoid it: let a top-level package connect them, or register handlers through function parameters.

3. Assuming the first package listed is the only problem

The error shows a chain, not just one bad package. The issue is the loop formed by all packages together.

Avoid it: inspect every package in the chain.

4. Fixing only one file without fixing package design

Even if you remove one import, another cycle may remain.

Your error already shows multiple cycles.

Avoid it: review the dependency graph, not only a single import statement.

5. Confusing package-level imports with function calls

Calling a function from another package is not itself the problem. The problem is the package import relationship.

6. Creating circular test dependencies

Test helpers can accidentally import the package under test through another package.

Broken example:

// account_test imports testutil
// testutil imports account

This may create a cycle depending on structure.

Avoid it: keep test helper packages independent, or place helpers locally in the same package when appropriate.

Comparisons

ConceptWhat it meansAllowed in Go?Example
One-way importOne package depends on another without looping backYesaccount -> base
Direct cycleTwo packages import each otherNoa -> b -> a
Indirect cycleA longer chain returns to the starting packageNoa -> b -> c -> a
Top-level compositionmain wires packages together without lower packages importing upwardYesmain -> mux, main -> account
Shared utility packageLower-level package used by many others, but does not import them backYescontrollers -> utils

Import cycle vs normal dependency

  • A normal dependency moves in one direction
  • An import cycle loops back to an earlier package

Refactor options compared

ApproachWhen to use itBenefit
Move shared code to a new packageMultiple packages need the same logicBreaks coupling
Use interfaces or function parametersOne package only needs behavior, not concrete package knowledgeReduces direct imports
Wire packages in mainApp startup connects componentsKeeps lower packages independent
Merge packagesTwo packages are too tightly coupled to separate cleanlySimplifies design when separation is artificial
Take Quiz

Cheat Sheet

How to read the error

If you see:

package A
    imports B
    imports C
    imports A

that means:

A -> B -> C -> A

Cycle found.

Rules

  • Go packages cannot form circular imports
  • Cycles can be direct or indirect
  • The full import graph must be acyclic
  • Tests can also reveal cycles

Quick debugging steps

  1. Start from the first package in the error
  2. Follow each imports line in order
  3. Stop when the path returns to a package already seen
  4. Inspect those packages' import statements
  5. Refactor so dependencies flow one way

Common fixes

  • Move shared code into a neutral package
  • Use main to wire dependencies together
  • Replace package knowledge with interfaces or function arguments
  • Remove imports from low-level packages to high-level packages
  • Merge packages if they are inseparable

Your specific cycles

account -> base -> mux -> account
base -> mux -> routes -> base

Good dependency direction

controllers -> services -> repositories
controllers -> shared/http
main -> controllers + router

Warning signs

  • base, common, or utils importing feature packages
  • router importing controllers while controllers also import router
  • initialization packages depending on feature packages and vice versa

FAQ

What does import cycle not allowed mean in Go?

It means your packages form a circular dependency. One package imports another package that eventually imports the first package again.

How do I find the cycle in the error message?

Read the packages in order from top to bottom. The cycle exists when the chain returns to a package already listed earlier.

Is the wrong dependency always the last import shown?

Not necessarily. The problem is the whole loop, not just one line. Any import in the cycle may need to be changed depending on package design.

Why does Go forbid circular imports?

They make code harder to compile, reason about, test, and maintain. Go encourages clear one-way package dependencies.

Can test files cause import cycles?

Yes. Test helpers or external test packages can introduce dependency paths that expose cycles.

Should I use interfaces to fix import cycles?

Sometimes, yes. If one package only needs behavior, using interfaces or callback functions can remove the need for a direct import.

Should I merge packages to fix a cycle?

Sometimes. If two packages are always used together and cannot be separated cleanly, merging them may be simpler than forcing an artificial boundary.

Take Quiz

Mini Project

Description

Build a small Go application structure with controllers and routing, then organize it so route registration does not create an import cycle. This demonstrates how to keep dependencies flowing in one direction.

Goal

Create a simple account route registration setup in Go without any circular package imports.

Requirements

  1. Create a mux package that can register a route and a handler function.
  2. Create an account package with a handler function.
  3. Avoid making mux import account.
  4. Use a top-level package such as main to connect the router and controller.
  5. Print the registered route to prove the setup works.
Take Quiz

Keep learning

Related questions

View more