Working Around Import Cycles in Go
If you've worked in a Go codebase long enough, you've probably run into this error:
import cycle not allowed
You've probably learned the hard way that if you have a package foo
that imports bar
:
package foo
import "bar"
You would not also be able to import bar
from foo
.
package bar
import "foo" // import cycle not allowed
I want to show you a pragmatic way we solved this problem without needing a dramatic change to the package structure.
How we're dealing with import cycles in our legacy code
Today, we're migrating our usages of our LegacyCustomer
to a new Customer
component.
package legacycustomer
type LegacyCustomerService struct {
// ...
}
func NewLegacyCustomerService() *LegacyCustomerService {
// returns a service that's used to create legacy customers
}
The newer Customer
component depends on LegacyCustomer
.
package customer
import "legacycustomer"
type CustomerService struct {
LegacyCustomerService *legacycustomer.LegacyCustomerService
}
func NewCustomerService(svc *legacycustomer.LegacyCustomerService) *CustomerService {
// returns a service that depends on LegacyCustomerService
// and is used to create customers in the new world.
}
We instantiate these services in the process that bootstraps the server.
package main
import (
"legacycustomer"
"customer"
)
func main() {
legacySvc := legacycustomer.NewLegacyCustomerService()
custSvc := customer.NewCustomerService(legacySvc)
// and so on...
}
To facilitate a migration, we need to dual-write Customer
s from LegacyCustomerService
.
We want to do something like the following code snippet, but this won't work.
package legacycustomer
import "customer" // import cycle!
type LegacyCustomerService struct {
CustomerService *customer.CustomerService
}
func NewLegacyCustomerService(svc *customer.CustomerService) *LegacyCustomerService {
// returns a service that's used to create legacy customers
// but will also start dual-writing new customers.
}
The customer
package already imported legacycustomer
,
so we can't import legacycustomer
from customer
.
We'll use dependency injection to avoid the need for a cyclic import. Here's how:
- Create an interface that
legacycustomer
will import. - Have
customer
implement that interface. - Let
legacycustomer
have a dependency on the interface, not theCustomerService
directly.
First, let's create the interface:
package api
import "legacycustomer"
type CustomerAPI interface {
CreateCustomerFromLegacyCustomer(c *legacycustomer.LegacyCustomer) error
}
Now, we'll let CustomerService
implement CustomerAPI
.
Unlike some languages like Java,
we don't have to state the fact that CustomerService
implements CustomerAPI
.
Go supports duck typing. So if it looks like a duck, and it quacks like a duck, then it must be a duck.
package customer
import "legacycustomer"
// Notice we say nothing about `CustomerAPI`, but we implement it nonetheless.
func (svc *CustomerService) CreateCustomerFromLegacyCustomer(c *legacycustomer.LegacyCustomer) error {
// ...
}
Now we can update legacycustomer
to use the interface:
package legacycustomer
import "api"
type LegacyCustomerService struct {
CustomerAPI api.CustomerAPI
}
// ...
And when we start the server, we can pass the implementation of the interface (i.e. CustomerService
) to legacycustomer
:
package main
import (
"legacycustomer"
"customer"
)
func main() {
legacySvc := legacycustomer.NewLegacyCustomerService()
custSvc := customer.NewCustomerService(legacySvc)
// this is the line that does the depenency injection.
// since `CustomerService` implements `CustomerAPI`, this is legal.
legacySvc.CustomerAPI = custSvc
// and so on...
}
Voilà. You now have a technique that leverages dependency injection and duck typing to help you work around cyclic imports in Go.