Say I have these two files in golang:
// main/a/a.go
import "main/b"
type Model struct {
ID int `json:"id"`
Me int `json:"me"`
You int `json:"you"`
}
func zoom(v b.Injection){
}
func Start(){
// ...
}
and then the second file looks like:
// main/b/b.go
import "main/a"
type Injection struct {
ModelA a.Model
}
func GetInjection() Injection {
return Injection{
ModelA: a.Start(),
}
}
so as you can see, these are circular imports, each file imports the other. So I need to use a 3rd file, and have these two files import the 3rd file.
But I am really struggling how to get this functionality and avoid cyclic imports.
My first step, is to move the Injection
type into a 3rd file:
// main/c/c.go
type Injection struct {
ModelA interface{} // formerly a.Model
}
so now this is what it looks like:
a imports c
b imports a,c
so no more cycles, however the problem is that I don't know how to create an interface for a.Model
in c.go
? An empty interface{}
like I used above doesn't work, for the normal reasons.
How do I solve this cyclic import problem with these 2 original files?
If you want them to put into separate packages, you can't have Model
and zoom()
in the same package, as zoom()
refers to Injection
and Injection
refers to Model
.
So a possible solution is to put Model
into package a
, zoom()
into package b
, and Injection
into package c
. c.Injection
can refer to a.Model
, b.zoom()
can refer to c.Injection
. There's no circle in this:
b.zoom() --------> c.Injection ---------> a.Model
I assume there are other references in your real code which are not in the question which may prevent this from working, but you can move "stuff" around between packages, or you can break it down into more.
Also, if things are coupled this "tight", you should really consider putting them into the same package, and then there is no problem to solve.
Another way to solve circular import issue is to introduce interfaces. E.g. if your zoom()
function would not refer to Injection
, the package containing Model
and zoom()
would not need to refer to Injection
's package.
Inspect what zoom()
needs to do with Injection
. If that is method calls, that's already good. If not, add methods to Injection
. Then you may define an interface in zoom()
's package containing the methods zoom()
needs to call, and change its parameter type to this interface. Implementing interfaces in Go is implicit, there is no declaration of intent. So you can remove the reference in the parameter type, still you will be able to pass Injection
values to zoom()
.
Also related, check Dave Cheney's thoughts about organizing code:
I believe code should be organised into packages names for what the package provides, not what it contains. This can sometimes be subtle, but usually not.
For example, http, provides http clients and servers.
As a counter example, package utils is a poor name, yes it provides utilities, but you have no idea what from the name, in truth this package is named for what it contains.
If your project is a library, it should contain one package (excluding examples and possibly utility commands), if it contains more packages, that is a sign that the library is trying to do too many things.
Prefer to avoid multiple packages by default, only split code by package if there is a clear separation of concerns. In my experience many frustrations with complex and possibly circular package structures are the result of too many packages in a project.