Overview

Staticcheck is a static analysis toolset for the Go programming language.
It comes with a large number of checks,
integrates with various Go build systems
and offers enough customizability to fit into your workflows.

Running staticcheck

Staticcheck can be run on code in several ways,
mimicking the way the official Go tools work.
At the core, it expects to be run on well-formed Go packages.
The most common way of specifying packages is via their import paths.
One or more packages can be specified in a single command,
and the ... glob operator is supported.
All of the following examples are valid invocations:

Note that all files of the package need to be specified,
similar to how go build works.

Configuration

Various aspects of staticcheck can be customized with configuration files.

These files are placed in Go packages and apply recursively to the package tree rooted at the containing package.
For example, configuration placed in pkg will apply to pkg, pkg/subpkg, pkg/subpkg/subsubpkg and so on.

Configuration files in subpackages can override or inherit from settings of configuration files higher up the package tree.
Staticcheck's default configuration is represented as the virtual root of the configuration tree and can be inherited from.

Configuration format

Staticcheck configuration files are named staticcheck.conf and contain TOML.

Any set option will override the same option from further up the package tree,
whereas unset options will inherit their values.
Additionally, the special value "inherit" can be used to inherit values.
This is especially useful for array values, as it allows adding and removing values to the inherited option.

The special value "all" matches all possible values.
Currently, this is only used when enabling checks.

Values prefixed with a minus sign,
such as "-S1000"
will exclude values from a list.
This can be used in combination with "all" to express "all but",
or in combination with "inherit" to remove values from the inherited option.

Options

A list of all options and their explanations can be found on the Options page.

Example configuration

The following example configuration is the textual representation of staticcheck's default configuration.

Command-line flags

In addition to configuration files, some aspects of staticcheck can be controlled via command-line flags.
These are settings that can vary between individual invocations or environments (CI, editors, ...) and shouldn't be stored in configuration files.

Flag

Description

-checks

Allows overriding the list of checks to run.
Has the same syntax as the checks setting
in configuration files.

Specifiy the list of checks which,
if they find any issues in your code,
should cause staticcheck to exit with a non-zero status.
This can be used, for example, to not fail your CI
pipeline because of possible code simplifications.

Show all problems found,
even those that were ignored by linter directives.

-tags

Similar to go build -tags,
allows specifiying the build tags to use.

-tests

Include tests in the analysis.

-version

Display the version of staticcheck and exit.

Targeting Go versions

By default, staticcheck will make suggestions that are correct for the current version of Go.
If you're wishing to support older versions of Go,
not all suggestions are applicable –
some simplifications are only valid for newer versions of Go
and deprecated functions may not have had viable alternatives in older versions.

To target a specific Go version you can use the -go command line flag.
For example, with -go 1.6, only suggestions that are valid for Go 1.6 will be made.

Ignoring problems

In general, you shouldn't have to ignore problems reported by staticcheck.
Great care is taken to minimize the number of false positives and subjective suggestions.
Dubious code should be rewritten and genuine false positives should be reported so that they can be fixed.

The reality of things, however, is that not all corner cases can be taken into consideration.
Sometimes code just has to look weird enough to confuse tools,
and sometimes suggestions, though well-meant, just aren't applicable.
For those rare cases, there are several ways of ignoring unwanted problems.

Line-based linter directives

The most fine-grained way of ignoring reported problems is to annotate the offending lines of code with linter directives.

The //lint:ignore Check1[,Check2,...,CheckN] reason directive
ignores one or more checks on the following line of code.
The reason is a required field
that must describe why the checks should be ignored for that line of code.
This field acts as documentation for other people (including future you) reading the code.

Let's consider the following example,
which intentionally checks that the results of two identical function calls are not equal:

SA4000 of staticcheck
will flag this code,
pointing out that the left and right side of == are identical –
usually indicative of a typo and a bug.

To silence this problem, we can use a linter directive:

func TestNewEqual(t *testing.T) {
//lint:ignore SA4000 we want to make sure that no two results of errors.New are ever the same
if errors.New("abc") == errors.New("abc") {
t.Errorf(`New("abc") == New("abc")`)
}
}

Maintenance of linter directives

It is crucial to update or remove outdated linter directives when code has been changed.
Staticcheck helps you with this by making unnecessary directives a problem of its own.
For example, for this (admittedly contrived) snippet of code

tmp.go:1:2: this linter directive didn't match anything; should it be removed?

Checks that have been disabled via configuration files
will not cause directives to be considered unnecessary.

File-based linter directives

In some cases, you may want to disable checks for an entire file.
For example, code generation may leave behind a lot of unused code,
as it simplifies the generation process.
Instead of manually annotating every instance of unused code,
the code generator can inject a single, file-wide ignore directive to ignore the problem.

File-based linter directives look a lot like line-based ones:

//lint:file-ignore U1000 Ignore all unused code, it's generated

The only difference is that these comments aren't associated with any specific line of code.
Conventionally, these comments should be placed near the top of the file.

Unlike line-based directives, file-based ones will not be flagged for being unnecessary.

Resource usage

Static analysis is a rather resource intensive process,
having to apply expensive algorithms on a lot of data.
Depending on the complexity of the checked code,
this can result in many gigabytes of memory usage and minutes (if not hours) of CPU time.

When using staticcheck, there are two different ways of running it:
one package per invocation, or many packages per invocation.
These two modes have opposite CPU and memory usage characteristics.

Checking one package per invocation will keep the memory usage low,
but may end up recompiling the same packages over and over again, wasting CPU time.

Checking many packages, on the other hand,
needs to keep more data in memory at once,
but can reuse compiled packages, saving CPU time.

The following tables will show the characteristics of the different modes.

Resource consumption in different operation modes

Mode

CPU usage

Memory usage

One package per invocation

High

Low

Many packages per invocation

Low

High

Using staticcheck to check the Go standard library

Mode

CPU time spent

Wall time spent

Max memory used

One package per invocation

1692%

2241%

28%

All packages in one invocation (reference value)

100%

100%

100%

The maximum memory usage of a single invocation depends
on the combined complexity of the checked packages and their dependencies.
The entire standard library combined requires roughly 2.31 GB of memory,
while the most complex package, net/http, only needs 640 MB.
Other packages, such as the tiny image/color/palette, may only need as little as 17 MB.

Checking packages individually reduces both the maximum and the average (over time) memory usage,
but requires a much greater amount of CPU time,
as shared dependencies have to be compiled over and over again.
You should use the method that best suits your available resources.