skies.dev

Working Around Import Cycles in Go

3 min read

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:

pkg/foo/foo.go
package foo

import "bar"

You would not also be able to import bar from foo.

pkg/bar/bar.go
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.

pkg/legacycustomer/service.go
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.

pkg/customer/service.go
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.

pkg/server/main.go
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 Customers from LegacyCustomerService. We want to do something like the following code snippet, but this won't work.

pkg/legacycustomer/service.go
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:

  1. Create an interface that legacycustomer will import.
  2. Have customer implement that interface.
  3. Let legacycustomer have a dependency on the interface, not the CustomerService directly.

First, let's create the interface:

pkg/api/customer.go
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.

pkg/customer/service.go
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:

pkg/legacycustomer/service.go
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:

pkg/server/main.go
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.

Hey, you! 🫵

Did you know I created a YouTube channel? I'll be putting out a lot of new content on web development and software engineering so make sure to subscribe.

(clap if you liked the article)

You might also like