Why Your Go Code Sucks and How Linters Can Save You
Have you ever wished for a mentor who could whisper in your ear, “YOU FORGOT THE RULE!” whenever you make a coding mistake?

Have you ever wished you had a mentor who could quietly remind you, “YOU FORGOT THE RULE!” every time you make a coding mistake?
Or perhaps you’re trying to establish team guidelines, but you notice that team members frequently break them, either because they’re new or they just don’t feel like adhering to them?
Linters are tools that can serve as that mentor, catching errors in your code and explaining what’s wrong. Let’s dive into a quick example:
Here, MyStruct isn’t as efficient as it could be. Its size could shrink from 32 to 24 bytes with the right configuration.
“Really, I can change the size of a struct?”
If that’s news to you, you might want to check out my other article: “What is Alignment? A Small Change for a Huge Impact.”
This guide aims to walk you through installing linter-aggregator, setting it up, and customizing it to your needs
1. What linter should I use?
Golint used to be the standard for checking Go code, but it’s no longer the go-to option. In its absence, new tools like revive, go-critic, govet,…
This has led to a sudden increase in different choices, making it sometimes hard to figure out what’s best for different needs and preferences.
“Huh, so which one now?”
Well, I’d like to point you toward golangci-lint. Despite its name suggesting it’s a single tool, it’s actually more of a linter bundle. Think of it as a one-stop shop for linters or you could even call it a linters aggregator.
I can’t tell you the exact number of linters it contains, but it’s inclusive of all the ones I’ve already mentioned, so that should be enough.
Before we delve into the the code analysis part, let’s handle the setup part. If you’ve got that covered, you’re welcome to skip ahead.
How to install?
When it comes to installation, the creators of golangci-lint
advise using a binary instead of the go get or go install commands. The following command should take care of that (I'm using v1.54.1):
# binary will be $(go env GOPATH)/bin/golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.1
# After installing, you can check the version with
golangci-lint --version
For a complete guide on how to install golangci-lint, you can head over to their official installation page
If you’re a VSCode user like myself, adding the following lines to your settings.json file should do the trick:
{
"go.lintTool": "golangci-lint",
"go.lintOnSave": "workspace"
}
You can also achieve this via the Preferences settings tab:
Now, on to the part you’ve been waiting for: the linters.
2. Linters
Right out of the gate, golangci-lint has a set of default linters activated.
This means you can start benefitting from it instantly without having to tangle with any complex configurations. Simply execute the command golangci-lint run
.
$ golangci-lint run
And if you’re using VSCode, it’s even simpler. Just save your file and the linters will run automatically, doing so will display various issues in your code, each tagged by the linter that caught it, like so:
main.go:30:2: SA4006: this value of `b` is never used (staticcheck)
b := MyStructB{}
^
parser/indexer/indexer.go:19:19: fieldalignment: struct with 48 pointer bytes could be 40 (govet)
type basicIndexer struct {
^
lab/packages/matomic/atomic_test.go:61:8: const `c` is unused (unused)
const c uint32 = 10
^
lab/packages/munsafe/unsafe_test.go:101:17: Error return value of `syscall.Syscall` is not checked (errcheck)
Look at the above result, you will see at the end of the message, there is the name of linter, for example staticcheck
, unused
, govet
, errcheck
.
“Okay, but what do these linters actually do? Is there some sort of guide?”
You can find details on each individual linter by running:
$ golangci-lint linters
This command will display a detailed description for each, like this:
errcheck: errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
gosimple (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false]
govet (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
ineffassign: Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
staticcheck (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
unused (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
There are too many linters to delve into here, but if you’re curious, feel free to explore the array of linters supported by golangci-lint. We’ll focus more on commonly used linters later on but for now, let’s leave it there.
“I’m interested in trying out some specific linters. How can I go about that?”
So, there are mainly two ways to toggle linters on and off with golangci-lint.
First up, you can use command-line arguments. If you’re keen on enabling a specific linter, you can use the -E
or --enable
flags. To disable one, go for -D
or --disable
.
For instance, if errcheck
has caught your eye, you can activate it like this:
golangci-lint run --disable-all -E errcheck
Alternatively, you can manage linters via a configuration file. Do note, however, that any command-line options will override the config file settings.
Here’s how an example .golangci.yaml
file might appear:
linters:
disable:
- errcheck
enable:
- reassign
- revive
- zerologlint
Remember, simply adding these settings won’t turn off the default linters. For that, you’d use the disable-all option:
linters:
disable-all: true # default = false
enable:
- reassign
- revive
- zerologlint
A word of caution: the enable-all
option may seem tempting, but it can slow down your IDE considerably.
3. How to choose linter for your team?
Alright, you’ve got a handle on what linters are and how to set them up. Now, what should be your go-to linters? Fortunately, golangci-lint sorts linters into categories, making your choice easier.
Preset
Let’s take a quick look at what each group is all about:
bugs: Scans for errors you might’ve missed.
comment: Ensure your public functions and types are well-explained.
complexity: Flags convoluted code that’s hard to maintain.
error: Specifically looks for potential issues before they’re actual problems.
format: Keeps your code layout clean and consistent.
import: Ensures you’re importing packages the right way.
metalinter: A mix of multiple linters for a thorough review.
module: Focuses on how your modules interact.
performance: Points out code that could slow things down.
sql: Concentrates on SQL related code, making sure your queries and operations are optimized and safe.
style: Looks at how readable and stylish your code is.
test: Makes sure your tests are solid and up to standard.
unused: Finds and flags code that’s just sitting there.
Feeling ready to enable some presets? You can use either the command line or a config file to do it:
# command
golangci-lint run -p bugs -p comment
# .golangci-lint.yaml
linters:
presets:
- bugs
- comment
- complexity
- error
- format
- import
- metalinter
- module
- performance
- sql
- style
- test
- unused
The gold question: “Which linters should I go for?”
You could, of course, start by checking out configurations others have used, but keep in mind those setups are tailored to their specific needs, not yours.
So, how can you determine which linters are ideal for your team?
One method is to enable all available linters, then deactivate the ones that don’t align with your project’s requirements. You can set enable-all: true
in your configuration, but be warned, this will really slow down your IDE.
And if something doesn’t seem applicable, just take that linter off the list:
linters:
enable-all: true
disable:
- containedctx
- depguard
- dogsled
After you’ve done this a few times, you’ll likely land on a combination of linters that feels “just right”. Even if golangci-lint
adds new options, you can still integrate the ones you find useful.
Also be aware that enabling all linters will make your IDE less responsive. Some linters may not flag any issues but will consume time analyzing your code.
Here’s my shortlist of recommended linters:
errcheck: Essential for catching errors you may have overlooked.
gofmt: Keeps your code consistently formatted according to Go’s standards.
govet: A basic but crucial tool for catching dubious constructs in Go code.
staticcheck: Offers a wide range of checks for potential issues.
gosimple: Helps clean up your code by highlighting opportunities for simplification.
ineffassign: Flags assignments that don’t actually get used.
unused: Identifies any declared variables, constants, or types that aren’t being used.
misspell: Catches spelling mistakes in your comments and docs.
gosec: Scans for security risks, crucial for keeping your code safe.
bodyclose: Reminds you to close HTTP response bodies, preventing resource leaks.
gocyclo: Highlights functions that might be overly complex.
gocognit: Points out functions that could be hard to maintain or understand.
gocritic: A very versatile linter that provides various checks and performance optimizations.
godot: Ensures your comments are easy to read by requiring ending punctuation.
prealloc: Recommends pre-allocating slices where possible for better performance.
revive: A customizable linter that offers extended features compared to golint.
So, you’ve got the basics down, now let’s go deeper into linters. There’s so much more to checkout.