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
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:
- standard library modules
- 3rd party modules
- same company, but not current project modules
- 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.
- Reformatting our imports. This is what I wrote about above.
- 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