Work in a team? Have 4 groups of imports in your go files

Do you want to organise your Go imports into 4 groups instead of 3? Here's how you can do it with a new version of gci and golangci-lint

A rack of bowling shoes in order.

Okay, okay, this is controversial!

Accepted wisdom is to have only three groups for imports. That’s how the built in goimports tool works. It makes sure that all the things that your code needs to import gets imported, and removes the imports that you don’t need in the file. The order is standard libraries up top, 3rd party modules middle section, and then modules from the same project as the go.mod file go to the bottom.

Enter “working in a company”

You work in a tech company and you have a bunch of repositories. Some of those depend on others, so you now have a choice to make.

Let’s suppose your project is named github.com/acme/anti-roadrunner. It’s a service that comes up with various ways to help trap the road runner finally ending the struggles of Wile. E. Coyote.

One of the dependencies of that system is an ordering one, which lives in a different repository: github.com/acme/ordering. Now, according to standard rules, this is how your imports would look like:

package main

import (
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/acme/anti-roadrunner/metrics"
    "github.com/acme/anti-roadrunner/tracing"
    "github.com/acme/ordering/service"
)

For most people this is going to be good enough.

I want to do better.

The 4 groups of imports

In my mind the ideal ordering of imported modules should look like this:

  1. standard library modules
  2. 3rd party modules
  3. same company, but not current project modules
  4. current project modules

The above would look like this:

package main

import (
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/acme/ordering/service"

    "github.com/acme/anti-roadrunner/metrics"
    "github.com/acme/anti-roadrunner/tracing"
)

To me it looks a lot cleaner, a lot easier to distinguish what’s what. It helps a lot more when you’re importing from a lot of other company repositories.

Making this actually work

The built in goimports and gofmt won’t be able to give you this. There’s a tool called gci, that I’ve already written about, that can, since version 0.3.0.

This update makes gci super customisable. Here’s the syntax for what you need to run on the command line after you installed it:

$ gci write -s Standard -s Default -s "Comment( company packages.):Prefix(github.com/acme)" -s "Prefix(github.com/acme/anti-roadrunner)" --NoInlineComments --NoPrefixComments ./main.go

Which would also insert a comment above the company packages, like so:

package main

import (
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    // company packages.
    "github.com/acme/ordering/service"

    "github.com/acme/anti-roadrunner/metrics"
    "github.com/acme/anti-roadrunner/tracing"
)

The --NoInlineComments would strip out the inline comments on an import. This is an inline comment:

import (
    "github.com/rs/zerolog/log" // this is a better logging solution.
)

And because we have a comment in the sections for write, we also need to add the --NoPrefixComments flag, otherwise we’ll end up with duplicated comment lines every time we run the command. This looks bad 😅:

package main

import (
    "fmt"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    // company packages.
    // company packages.
    // company packages.
    // company packages.
    // company packages.
    // company packages.
    "github.com/acme/ordering/service"

    "github.com/acme/anti-roadrunner/metrics"
    "github.com/acme/anti-roadrunner/tracing"
)

Use it in your CI/CD pipeline

There are two main uses of this tool.

  1. Reformatting our imports. This is what I wrote about above.
  2. Making sure that code is formatted the “correct” way.

This second point is important for working in a company when it comes to code quality.

I tend to use gci as part of my golangci-lint configuration. Version 1.44.2 included the correct version of gci with the correct configuration.

I then put the golangci-lint into a Github action, and have it run on every PR opened and every merge into the main branch.

Here’s the configuration you need in your .golangci.yaml file for the specific linter. The entire other part of it is omitted:

linters-settings:
  gci:
    sections:
      - standard
      - default
      - comment( company packages.):prefix(github.com/acme)
      - prefix(github.com/acme/anti-roadrunner)

You can then use the official golangci-lint Github Action in your repository, and you should be set up!

Go forth and lint!

Photo by Priscilla Du Preez on Unsplash