Import cycle not allowed in Go: How to Break the Vicious Circle

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Import cycle not allowed: When Go puts up a roadblock for your code

\n\n

Every Go developer eventually encounters this phrase. The compiler abruptly stops the build, issuing a dry message: import cycle not allowed. It seems the program is logical and the packages are connected "as they should be." But Go thinks otherwise.

\n\n

This error is not a language whim, but a protection against chaos. Cyclic dependencies turn code into spaghetti, complicate testing, and make the build unpredictable. In this article, we will analyze why import cycle not allowed occurs, and compare three approaches to eliminating it: through interfaces, through a mediator package, and through refactoring. You will learn which method to choose depending on the situation.

\n\n

Why does Go forbid circular imports?

\n\n

Unlike languages with dynamic loading (Python, JavaScript), Go is compiled statically. The compiler builds a package dependency graph once. If package A imports package B, and B imports A — a cycle occurs. Go cannot resolve this order, so it aborts the compilation.

\n\n

A real-world example: the users package works with users, the orders package works with orders. In orders, user information is needed, and in users, a list of orders is needed. Result: import cycle not allowed.

\n\n

Typical scenario: Two packages that are "too friendly"

\n\n

Let's consider a classic situation. We have two packages: users and notifier.

\n\n

Package users (file users.go):

\n
package users\n\nimport "notifier"\n\ntype User struct {\n    Name  string\n    Email string\n}\n\nfunc (u *User) SendWelcome() {\n    notifier.Notify(u.Email, "Welcome!")\n}\n
\n\n

Package notifier (file notifier.go):

\n
package notifier\n\nimport "users"\n\nfunc Notify(email, msg string) {\n    // send notification\n}\n\nfunc GetUserEmail(user *users.User) string {\n    return user.Email\n}\n
\n\n

The compiler will output: import cycle not allowed: users -> notifier -> users. Both packages try to import each other. What to do?

\n\n

Approach 1: Interfaces — "Divide and Conquer"

\n\n

The most elegant way is to move the dependency to the interface level. Go loves explicit contracts.

\n\n

Create a contracts package where we define the Notifier interface:

\n
// contracts/contracts.go\npackage contracts\n\ntype Notifier interface {\n    Notify(email, msg string)\n}\n
\n\n

Now users does not depend on notifier directly:

\n
// users/users.go\npackage users\n\nimport "contracts"\n\ntype User struct {\n    Name  string\n    Email string\n}\n\nfunc (u *User) SendWelcome(n contracts.Notifier) {\n    n.Notify(u.Email, "Welcome!")\n}\n
\n\n

And notifier implements the interface:

\n
// notifier/notifier.go\npackage notifier\n\ntype EmailNotifier struct{}\n\nfunc (e *EmailNotifier) Notify(email, msg string) {\n    // sending logic\n}\n
\n\n

Pros:

\n
    \n
  • Complete absence of circular imports.
  • \n
  • Easy testing: the implementation can be swapped.
  • \n
  • Loose coupling.
  • \n
\n

Cons:

\n
    \n
  • Requires an additional package with interfaces.
  • \n
  • Sometimes interfaces are unnecessary for simple projects.
  • \n
\n\n

Approach 2: Mediator Package

\n\n

If interfaces seem excessive, you can create a third package that acts as a "bridge" between the two conflicting packages.

\n\n

Create a bridge package:

\n
// bridge/bridge.go\npackage bridge\n\nimport (\n    "notifier"\n    "users"\n)\n\nfunc SendWelcomeToUser(user *users.User) {\n                

Blogs

Book Recommendations