Fixing import cycle in Go
Asked Answered
A

5

20

So I have this import cycle which I'm trying to solve. I have this following pattern:

view/
- view.go
action/
- action.go
- register.go

And the general idea is that actions are performed on a view, and are executed by the view:

// view.go
type View struct {
    Name string
}

// action.go
func ChangeName(v *view.View) {
    v.Name = "new name"
}

// register.go
const Register = map[string]func(v *view.View) {
    "ChangeName": ChangeName,
}

And then in view.go we invoke this:

func (v *View) doThings() {
    if action, exists := action.Register["ChangeName"]; exists {
        action(v)
    }
}

But this causes a cycle because View depends on the Action package, and vice versa. How can I solve this cycle? Is there a different way to approach this?

Appointment answered 22/6, 2018 at 10:32 Comment(1)
Does this answer your question? Import cycle not allowedEvapotranspiration
A
32

An import cycle indicates a fundamentally faulty design. Broadly speaking, you're looking at one of the following:

  • You're mixing concerns. Perhaps view shouldn't be accessing action.Register at all, or perhaps action shouldn't be responsible for changing the names of views (or both). This seems the most likely.
  • You're relying on a concretion where you should be relying on an interface and injecting a concretion. For example, rather than the view accessing action.Register directly, it could call a method on an interface type defined within view, and injected into the View object when it is constructed.
  • You need one or more additional, separate packages to hold the logic used by both the view and action packages, but which calls out to neither.

Generally speaking, you want to architect an application so that you have three basic types of packages:

  1. Wholly self-contained packages, which reference no other first-party packages (they can of course reference the standard library or other third-party packages).
  2. Logic packages whose only internal dependencies are of type 1 above, i.e., wholly self-contained packages. These packages should not rely on each other or on those of type 3 below.
  3. "Wiring" packages, which mostly interact with the logic packages, and handle instantiation, initialization, configuration, and injection of dependencies. These can depend on any other package except for other type 3 packages. You should need very, very few packages of this type - often just one, main, but occasionally two or three for more complex applications.
Adelaidaadelaide answered 22/6, 2018 at 14:2 Comment(4)
Perfect, this gives me some food for thought.Appointment
I unintentionally downvoted this answer and now I have to wait some edition to undo my mistake. Sorry.Latham
I intentionally downvoted because of the attitude in this answer. The only fundamentally faulty design here is Go compiler's. There are tons of languages out there which handle this problem much better and don't pass the burden on developers. By the way this site is full of answers that say you are doing it wrong, you can't possibly know better than language designers; yet a few years later language implements the feature or fixes the problem. No need to try to find artificial excuses for the language's shortcomings.Ignoble
@YusufTarıkGünaydın whether the compiler is faulty is a matter of opinion, but in the context given here, this is a fundamentally faulty application design because it cannot compile in the language it's written in. The faulty design could also be fixed by rewriting everything in a language that allows for import cycles, but that is likely to be more work than just breaking the cycle, and doesn't seem like a particularly helpful answer to give.Adelaidaadelaide
N
3

Basically, you are able to break dependencies by introducing interface and injecting the interface instead of a struct.

With your example it would look like:

// view.go
package view

import "import_cycles/action"

type View struct {
    Name string
}

func (v *View) ModifyName(name string) {
    v.Name = name
}

func (v *View) DoThings() {
    if action, exists := action.Register["ChangeName"]; exists {
        action(v)
    }
}

// action.go
package action

func ChangeName(v NameChanger) {
    v.ModifyName("new name")
}

// register.go
package action

type NameChanger interface {
    ModifyName(name string)
}

var Register = map[string]func(v NameChanger){
    "ChangeName": ChangeName,
}

Please note that NameChanger interface is introduced. Here following things to be point:

  • this interface injected in function ChangeName instead of passing struct
  • struct View is implementing this interface

As a result package "action" no more need to import package "view" since the interface is placed in the same package "action"

In main.go we can test the result:

v := &view.View{
    Name: "some name",
}
v.DoThings()
fmt.Println(v)

// &{new name}
Napoli answered 26/4, 2022 at 16:50 Comment(0)
S
2

In my own case, I created a simple import cycle in my unit tests. My normal app was fine.

import cycle simple

To answer your question on fixing, first I isolated the function that caused the import cycle. In my case, the import cycle only happened when running tests.

Then I checked what type of import cycle. This helped to visualize the error. I found that Package B tests depended on Package A.

I moved the tests into Package A and no more import cycle [ and cleaner tests ].

Scaleboard answered 13/6, 2022 at 12:45 Comment(0)
K
0

Import cycles are the result of a design error. Structs which depend on each other in both directions must be in the same package, or else an import cycle will occur. By the way, Go is not the only programming language with this restriction. It also exist in C++ and Python, for example.

Knawel answered 22/6, 2018 at 10:54 Comment(6)
Indeed, I'm asking what my design error is though and how I can approach it differentlyAppointment
Read the second part of my answer, there is how to approach this problem.Knawel
I know that I can put them in the same package. I'd rather create a more well thought out system where I can decouple both of these rather than cramming them in the same packageAppointment
There is no other way, structs which point to each other must be in the same package.Knawel
@BertVerhees And by the way, Go actually is the only language that does this. In both C++ and python you can import single files. This is an enormous difference to having to import entire packages. But maybe its just my old mindset, coming from other languages. So far I havent found a solution except for this, which is not ideal.Richard
I don't agree with that, what if you need to have an application struct or a context struct that have a reference to a NetClientService in the net package and an IOHandler in the io package, what should we do, put everything together in the same package? it does not seem correct to me. In Python and C++ There are workarounds for this unavoidable scenario(forward declaration, and reference by class name(string) at least in Django)Acclimate
A
0

one way to solve cyclic dependency in by dependency injection, by this you break direct dependency of one package to other.

try injecting dependency from the main.go file

Alternation answered 21/6 at 13:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.