By Dominik Honnef, author of Staticcheck.

Staticcheck is a state of the art linter for the Go programming language. Using static analysis, it finds bugs and performance issues, offers simplifications, and enforces style rules.

Its checks have been designed to be fast, precise and useful. When Staticcheck flags code, you can be sure that it isn’t wasting your time with unactionable warnings. While checks have been designed to be useful out of the box, they still provide configuration where necessary, to fine-tune to your needs, without overwhelming you with hundreds of options.

Staticcheck can be used from the command line, in continuous integration (CI), and even directly from your editor.

Staticcheck is open source and offered completely free of charge. Sponsors guarantee its continued development. The play-with-go.dev project is proud to sponsor the Staticcheck project. If you, your employer or your company use Staticcheck please consider sponsoring the project.

This guide gets you up and running with Staticcheck by analysing the pets module.

Prerequisites

You should already have completed:

This guide is running using:

$ go version
go version go1.19.1 linux/amd64

Installing Staticcheck

In this guide you will install Staticcheck to your PATH. For details on how to add development tools as a project module dependency, please see the “Developer tools as module dependencies” guide.

Use go get to install Staticcheck:

$ go install honnef.co/go/tools/cmd/staticcheck@v0.3.3
go: downloading honnef.co/go/tools v0.3.3
go: downloading golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f
go: downloading golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e
go: downloading golang.org/x/sys v0.0.0-20211019181941-9d821ace8654
go: downloading github.com/BurntSushi/toml v0.4.1
go: downloading golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4

Note: so that this guide remains reproducible we have spcified an explicit version, v0.3.3. When running yourself you could use the special version latest.

The rather ugly use of a temporary directory ensures that go get is run outside of a module. See the “Setting up your PATH section in Installing Go to ensure your PATH is set correctly.

Check that staticcheck is on your PATH:

$ which staticcheck
/home/gopher/go/bin/staticcheck

Run staticcheck as a quick check:

$ staticcheck -version
staticcheck 2022.1.3 (v0.3.3)

You’re all set!

Create the pets module

Time to create an initial version of the pets module:

$ mkdir /home/gopher/pets
$ cd /home/gopher/pets
$ go mod init pets
go: creating new go.mod: module pets

Because you are not going to publish this module (or import the pets package; it’s just a toy example), you do not need to initialise this directory as a git repository and can give the module whatever path you like. Here, simply pets.

Create an inital version of the pets package in pets.go:

package pets

import (
	"errors"
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n")
	default:
		return errors.New(fmt.Sprintf("Cannot take %v for a walk", p.Name))
	}
	return nil
}

func (self Pet) String() string {
	return fmt.Sprintf("%s", self.Name)
}

This code looks sensible enough. Build it to confirm there are no compile errors:

$ go build

All good. Or is it? Let’s run Staticcheck to see what it thinks.

Staticcheck can be run on code in several ways, mimicking the way the official Go tools work. At its core, it expects to be run on well-formed Go packages. So let’s run it on the current package, the pets package:

$ staticcheck .
pets.go:23:14: Printf format %v reads arg #1, but call has only 0 args (SA5009)
pets.go:25:10: should use fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...)) (S1028)
pets.go:30:7: receiver name should be a reflection of its identity; don't use generic names such as "this" or "self" (ST1006)
pets.go:31:9: the argument is already a string, there's no need to use fmt.Sprintf (S1025)

Oh dear, Staticcheck has found some issues!

As you can see from the output, Staticcheck reports errors much like the Go compiler. Each line represents a problem, starting with a file position, then a description of the problem, with the Staticcheck check number in parentheses at the end of the line.

Staticcheck checks fall into different categories, with each category identified by a different code prefix. Some are listed below:

  • Code simplification S1???
  • Correctness issues SA5???
  • Stylistic issues ST1???

The Staticcheck website lists and documents all the categories and checks. Many of the checks even have examples. You can also use the -explain flag to get details at the command line:

$ staticcheck -explain SA5009
Invalid Printf call

Available since
    2019.2

Online documentation
    https://staticcheck.io/docs/checks#SA5009

Let’s consider one of the problems reported, ST1006, documented as “Poorly chosen receiver name”. The Staticcheck check documentation quotes from the Go Code Review Comments wiki:

The name of a method’s receiver should be a reflection of its identity; often a one or two letter abbreviation of its type suffices (such as “c” or “cl” for “Client”). Don’t use generic names such as “me”, “this” or “self”, identifiers typical of object-oriented languages that place more emphasis on methods as opposed to functions. The name need not be as descriptive as that of a method argument, as its role is obvious and serves no documentary purpose. It can be very short as it will appear on almost every line of every method of the type; familiarity admits brevity. Be consistent, too: if you call the receiver “c” in one method, don’t call it “cl” in another.

Each error message explains the problem, but also indicates how to fix the problem. Let’s fix up pets.go:

package pets

import (
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) String() string {
	return p.Name
}

And re-run Staticcheck to confirm:

$ staticcheck .

Excellent, much better.

Configuring Staticcheck

Staticcheck works out of the box with some sensible, battle-tested defaults. However, various aspects of Staticcheck can be customized with configuration files.

Whilst fixing up the problems Staticcheck reported, you notice that the pets package is missing a package comment. You also happened to notice on the Staticcheck website that check ST1000 covers exactly this case, but that it is not enabled by default.

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

Let’s create a Staticcheck configuration file to enable check ST1000, inheriting from the Staticcheck defaults:

checks = ["inherit", "ST1000"]

Re-run Staticcheck to verify ST1000 is reported:

$ staticcheck .
pets.go:1:1: at least one file in a package should have a package comment (ST1000)

Excellent. Add a package comment to pets.go to fix the problem:

// Package pets contains useful functionality for pet owners
package pets

import (
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) String() string {
	return p.Name
}

Re-run Staticcheck to confirm there are no further problems:

$ staticcheck .

Ignoring problems

Before going much further, you decide it’s probably a good idea to be able to feed a pet, and so make the following change to pets.go:

// Package pets contains useful functionality for pet owners
package pets

import (
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) Feed(food string) {
	food = food
	fmt.Printf("Feeding %v some %v\n", p.Name, food)
}

func (p Pet) String() string {
	return p.Name
}

Re-run Staticcheck to verify all is still fine:

$ staticcheck .
pets.go:31:2: self-assignment of food to food (SA4018)

Oops, that was careless. Whilst it’s clear how you would fix this problem (and you really should!), is it possible to tell Staticcheck to ignore problems of this kind?

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.

This is not a rare or corner case, but let’s use it as an opportunity to demonstrate linter directives.

The most fine-grained way of ignoring reported problems is to annotate the offending lines of code with linter directives. Let’s ignore SA4018 using a line directive, updating pets.go:

// Package pets contains useful functionality for pet owners
package pets

import (
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) Feed(food string) {
	//lint:ignore SA4018 trying out line-based linter directives
	food = food
	fmt.Printf("Feeding %v some %v\n", p.Name, food)
}

func (p Pet) String() string {
	return p.Name
}

Verify that Staticcheck no longer complains:

$ staticcheck .

In some cases, however, 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.

Let’s change the line-based linter directive to a file-based one in pets.go:

// Package pets contains useful functionality for pet owners
package pets

import (
	"fmt"
)

//lint:file-ignore SA4018 trying out file-based linter directives

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) Feed(food string) {
	food = food
	fmt.Printf("Feeding %v some %v\n", p.Name, food)
}

func (p Pet) String() string {
	return p.Name
}

Verify that Staticcheck continues to ignore this check:

$ staticcheck .

Great. That’s both line and file-based linter directives covered, demonstrating how to ignore certain problems.

Finally, let’s remove the linter directive, and fix up your code:

// Package pets contains useful functionality for pet owners
package pets

import (
	"fmt"
)

type Animal int

const (
	Dog Animal = iota
	Snake
)

type Pet struct {
	Kind Animal
	Name string
}

func (p Pet) Walk() error {
	switch p.Kind {
	case Dog:
		fmt.Printf("Will take %v for a walk around the block\n", p.Name)
	default:
		return fmt.Errorf("cannot take %v for a walk", p.Name)
	}
	return nil
}

func (p Pet) Feed(food string) {
	fmt.Printf("Feeding %v some %v\n", p.Name, food)
}

func (p Pet) String() string {
	return p.Name
}

And check that Staticcheck is happy one last time:

$ staticcheck .

We can now be sure of lots of happy pets!

Conclusion

This guide has provided you with an introduction to Staticcheck, and the power of static analysis. To learn more see:

As a next step you might like to consider: