0

I have encountered an import cycle problem. I read some blogs but didn't understand them. This is the sample code that I have written.

file1.go

package dir1

type Filo interface {
    File2(string) string
}

func File1(message string) string {
    var f Filo
    importFile2 := f.File2("file 2")
    return "Welcome to " + message + " and message from file 2: " + importFile2
}

file2.go

package dir2

import "github.com/ibilalkayy/app/dir1"

func File2(message string) string {
    importFile1 := dir1.File1("file 1")
    return "Welcome to " + message + " and message from file 1: " + importFile1
}

main.go

package main

import (
    "fmt"

    "github.com/ibilalkayy/app/dir1"
    "github.com/ibilalkayy/app/dir2"
)

func main() {
    fmt.Println("Hello world")
    first := dir1.File1("file 1")
    second := dir2.File2("file 2")
    fmt.Println(first)
    fmt.Println(second)
}
  • 1
    You can't have two packages that mutually depend on each other. Put both `File1` and `File2` in one package. – Erwin Bolwidt Feb 01 '23 at 04:53
  • 1
    The example is contrived, so hard to say. Put them in the same package. Or, define a function variable in each package and call that instead of importing the other package. Initialize them in main. – Burak Serdar Feb 01 '23 at 04:54
  • 2
    Your example is not a realistic example. It is designed to have mutually dependent packages. Such closely coupled code should be in the same package. Sometimes you do need to call functions/methods like this, and that's when you use function variables or interfaces to solve it. – Burak Serdar Feb 01 '23 at 05:06
  • @BurakSerdar You're right. I read some blogs about interfaces but didn't understand them. I don't know how to solve this problem using interface. – Kaifah Habibi Feb 01 '23 at 05:09
  • You can't solve this particular case with an interface ,because you don't have methods. You can solve it with a function variable. – Burak Serdar Feb 01 '23 at 05:10
  • Paraphrasing the other comments: your sample you pasted in your question is too simple to reflect a real need to have 2 separate packages. I would second the suggestion to put `File1()` and `File2()` functions in the same package, and you would also quickly witness that, with the current code, calling any of the two functions will lead to an endless loop and a stack overflow. Can you edit your question to describe something that is closer to your actual need ? – LeGEC Feb 01 '23 at 05:11
  • @LeGEC This is just an example because the project I am working on is big in which MySQL and Redis files are causing the import cycle problem. They are distributed in different packages. – Kaifah Habibi Feb 01 '23 at 05:13
  • @LeGEC Can you show me how you solve it using function variable because I don't know about it. – Kaifah Habibi Feb 01 '23 at 05:14
  • you mention MySQl and Redis : when I am faced with packages that are split based on 2 such separate components like these, and the way to write code starts to "not work" because it looks like my sql functions need redis and my redis functions need mysql, one pretty general approach is to turn this into 3 packages: one with mysql treatments only, one with redis treatments only, and a 3rd one, which imports the first two, and contains functions which combine interactions with both. – LeGEC Feb 01 '23 at 05:19
  • Can you show me an example of it using this code? I know what you're talking about but how to implement it, I don't an idea about it. – Kaifah Habibi Feb 01 '23 at 05:22
  • Related: https://stackoverflow.com/questions/67856223/how-can-i-pinpoint-an-import-cycle-not-allowed-problem/67856593#67856593 – jub0bs Feb 01 '23 at 06:14

1 Answers1

4

The desire for an import cycle is usually an indication there is something wrong with the design.

There are 2 general solutions to avoid import cycles:

  • If 2 packages are so closely tied and they depend on each other, it is probably better to merge them (or core functionality) into a single package. This avoids import cycles.
  • Alternatively, one of the packages should use an interface so it does not depend on the other package. This creates a natural ordering of package layers.

For example, the following contrived example won't compile due to the import cycle:

package main

import "example.com/base"
import "example.com/other"

func main() {
   f := &other.Foo{}
   _ = base.Process(f)
}
package base

import "example.com/other"

func Process(f *other.Foo) {
    for {
        if err := f.Frob(); err != nil {
            return err
        }
    }
}
package other

import "example.com/base"

type Foo int

func (f *foo) Frob(name string) error {
    if *f == 0 {
        return errors.New("completed")
    }
    *f--
    return base.Process(f)
}

This can be fixed by changing base to use an interface:

package base

type Frobber interface {
    Frob(string) error
}

func Process(f Frobber) error {
    for {
        if err := f.Frob("some file"); err != nil {
            return err
        }
    }
}

This works since *other.Foo can to passed to base.Process without the base package needing to know anything about the other package.

Another option is to use function variables:

// Given:
package other
import "example.com/base"
type Foo struct {}
func (f *Foo) Frob(name string) error

package base
import "example.com/other"
func Process(f *Foo) error

// Use this instead:
package base
func Process(fn func(string) error) error

// Which enables using:
package main
import "example.com/other"
func main() {
    f := &other.Foo{} // NOTE: This needs to be real an initialised.
    Process(f.Frob)
}

The example in the question is fundamentally broken beyond the import cycle. It will run out of stack space due to mutual recursion. Ignoring that, you can get it to compile with an untyped func. Eg:

func File1(fn func(string) string) string {
    return "Welcome to message from file 2: " + fn("some file")
}

// Call with:

func something(name string) string {
   return "foo/" + name
}

dir1.File1(something)
mpx
  • 1,168
  • 1
  • 13
  • I tried it using my code but it is giving me `panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4810d2]` – Kaifah Habibi Feb 01 '23 at 05:52
  • Most likely you are using an interface, but haven't assigned anything to it. The "zero" value for an interface is nil. Hence trying to deference or call an interface with nothing assigned will generate SIGSEGV. If you can't work it out, I'd recommend adding an example showing the error you are seeing. – mpx Feb 01 '23 at 06:08
  • I have updated the code above. You can check it out. – Kaifah Habibi Feb 01 '23 at 06:13
  • `var f Filo importFile2 := f.File2()` is broken. You have an empty interface value. You need to pass a value into the function that is non-nil. See my example. – mpx Feb 01 '23 at 06:27
  • I didn't get your example. This is the point that I was sure, will give me an error. I don't know how to assign a value to `var f Filo` – Kaifah Habibi Feb 01 '23 at 06:30
  • I'd strongly recommend creating an example of what you want within a single package that actually works. After that it will be easy to show how to split it. Your example code doesn't make any sense, so it's hard to show how it could be done "correctly." I've updated my answer showing a more specific example with a function type. – mpx Feb 01 '23 at 06:35
  • Regarding my example, notice that `base.Process` takes an interface as a parameter, and the `other` package calls `base.Process` with a concrete value that implements the interface. You need to do the same. Or pass in a function that implements the function signature. – mpx Feb 01 '23 at 06:36
  • Sorry I didn't get what are you talking about. – Kaifah Habibi Feb 01 '23 at 06:38
  • The `Frob()` function in your code does not contain the parameter. Only the `Process()` does. In my case, both of the functions contain the parameters. – Kaifah Habibi Feb 01 '23 at 06:48
  • I've updated my examples to include a parameter. – mpx Feb 01 '23 at 06:53
  • I have updated the code. You can check it but it is still giving me the `panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x480fbf]` – Kaifah Habibi Feb 01 '23 at 07:06
  • You can't use `var f Filo` by itself. It's uninitialised. Please read about interfaces (eg, https://gobyexample.com/interfaces https://golangbot.com/interfaces-part-1/ ). In my example the function has an interface parameter and a real value is passed in. You need to do the same. – mpx Feb 01 '23 at 07:11
  • I have used it like this `importFile2 := f.File2("file 2")` – Kaifah Habibi Feb 01 '23 at 07:12
  • ....and `f` is nil. Hence calling `f.File2` segfaults. – mpx Feb 01 '23 at 07:12
  • If I type `f Filo` in the function parameter then when I call this in another file like this `dir1.File1("file 2", "file 1")`, it gives me an error `cannot use "file 2" (constant of type string) as dir1.Filo value in argument to dir1.File1: string does not implement dir1.Filo` – Kaifah Habibi Feb 01 '23 at 07:16
  • "file 2" is a string. It does not implement the `dir1.Filo` interface. The value I pass into the interface _implements_ the interface. You need to do the same. Please consider working through some Go tutorials (Eg, https://gophercises.com/ https://exercism.org/tracks/go ). Your questions are much broader than "how to break import cycles". – mpx Feb 01 '23 at 07:19
  • Where in the `Frob()` function `*foo` came from? – Kaifah Habibi Feb 01 '23 at 07:37
  • Added the `main` package at the top to show how to initialise the value passed into `Process`. With `func (f *Foo) Frob() error`, `*Foo` is the method receiver. It is initalised wherever you initialise a value of type `*other.Foo`. Please review some Go tutorials. This is fundamental to how methods and interfaces work. – mpx Feb 01 '23 at 08:02