Overview

Staticcheck is a suite of tools for analyzing and improving Go code.

At its core sits the name-giving staticcheck tool, which is responsible for finding bugs, inefficiencies and crashes that are waiting to happen – before they happen.

It is complemented by gosimple, which simplifies code, and unused, which finds code that isn't used anymore. Together, these tools aim to keep your code base nice and tidy as it evolves.

For companies, stylecheck provides a customizable style checker, helping you automate enforcing internal style guides, without wasting energy hunting down style violations during code reviews.

Running the tools

The staticcheck tools can be run on code in several ways, mimicking the way the official Go tools work. At the core, they expect 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:

staticcheck github.com/example/foo
staticcheck github.com/example/foo github.com/example/bar
staticcheck github.com/example/...

In addition, a single package can be specified as a list of files:

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

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. In addition, if any package in an invocation fails to compile, the whole invocation fails.

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

Resource consumption in different operation modes
Mode CPU usage Memory usage Can skip broken packages
One package per invocation High Low Yes
Many packages per invocation Low High No
Using staticcheck to check the Go standard library
Mode CPU time spent Wall time spent Max memory used
One package per invocation 863 s 381 s 0.64 GB
All packages in one invocation 51 s 17 s 2.31 GB

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.

Megacheck

Megacheck is a small wrapper that runs all of the different code checkers in one go, greatly reducing the amount of time it takes to check packages. Whenever you intend to run more than one of the code checkers, you should use megacheck - it can save over 25% of CPU time.

By default, megacheck runs all of the checkers. However, individual checkers can be disabled by using the -<checker>.enabled=false flags. For example, -simple.enabled=false will disable gosimple. By using -<checker>.exit-non-zero you can control which checkers will cause megacheck to exit non-zero when problems are found.

All of the checkers' individual flags are accessible by prefixing them with -<checker>., like for example -unused.exported.

Targeting Go versions

By default, the staticcheck tools 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 have the tools 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 the staticcheck suite of tools. 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:

func TestNewEqual(t *testing.T) {
  if errors.New("abc") == errors.New("abc") {
    t.Errorf(`New("abc") == New("abc")`)
  }
}

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

//lint:ignore SA1000 we love invalid regular expressions!
regexp.Compile(".+")
staticcheck will report the following:
tmp.go:1:2: this linter directive didn't match anything; should it be removed?

Each tool will only complain about the checks that it owns – e.g. gosimple won't complain about the earlier example. Furthermore, checks that have been disabled entirely 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.