By Dominik Honnef, author of Staticcheck.
Staticcheck is a state of the art linter for the Go programming language. Using staticanalysis, it finds bugs and performance issues, offerssimplifications, 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’twasting your time with unactionable warnings. While checks have been designed to be useful out of the box, they stillprovide 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 youreditor.
Staticcheck is open source and offered completely free of charge. Sponsors guarantee its continued development. Theplay-with-go.dev
project is proud to sponsor the Staticcheck project. If you, your employer or your company useStaticcheck 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:
- Go fundamentals
This guide is running using:
$ go versiongo 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 projectmodule 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.3go: downloading honnef.co/go/tools v0.3.3go: downloading golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501fgo: downloading golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936ego: downloading golang.org/x/sys v0.0.0-20211019181941-9d821ace8654go: downloading github.com/BurntSushi/toml v0.4.1go: 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 -versionstaticcheck 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 petsgo: creating new go.mod: module pets
Because you are not going to publish this module (or import the pets
package; it’s just a toyexample), you do not need to initialise this directory as a git
repository and can give the module whatever path youlike. Here, simply pets
.
Create an inital version of the pets
package in pets.go
:
package petsimport ("errors""fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 tobe 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 theend of the line.
Staticcheck checks fall into different categories, with each category identified by a different code prefix. Some arelisted below:
- Code simplification
S1???
- Correctness issues
SA5???
- Stylistic issues
ST1???
The Staticcheck website lists and documents all the categories and checks. Many ofthe checks even have examples. You can also use the -explain
flag to get details at the commandline:
$ staticcheck -explain SA5009Invalid Printf callAvailable since 2019.2Online documentation https://staticcheck.io/docs/checks#SA5009
Let’s consider one of the problems reported, ST1006
, documented as “Poorlychosen receiver name”. The Staticcheck check documentation quotes from the Go Code Review Commentswiki:
The name of a method’s receiver should be a reflection of itsidentity; often a one or two letter abbreviation of its typesuffices (such as “c” or “cl” for “Client”). Don’t use genericnames such as “me”, “this” or “self”, identifiers typical ofobject-oriented languages that place more emphasis on methods asopposed to functions. The name need not be as descriptive as thatof a method argument, as its role is obvious and serves nodocumentary purpose. It can be very short as it will appear onalmost every line of every method of the type; familiarity admitsbrevity. Be consistent, too: if you call the receiver “c” in onemethod, 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 petsimport ("fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 canbe customized with configuration files.
Whilst fixing up the problems Staticcheck reported, you notice that the pets
package is missing a packagecomment. You also happened to notice on the Staticcheck website that checkST1000
covers exactly thiscase, but that it is not enabled by default.
Staticcheck configuration files are named staticcheck.conf
and containTOML.
Let’s create a Staticcheck configuration file to enable check ST1000
, inheriting from theStaticcheck 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 ownerspackage petsimport ("fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 followingchange to pets.go
:
// Package pets contains useful functionality for pet ownerspackage petsimport ("fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 = foodfmt.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 totell 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 offalse positives and subjective suggestions. Dubious code should be rewritten and genuine false positives should bereported 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 tolook weird enough to confuse tools, and sometimes suggestions, though well-meant, just aren’t applicable. For those rarecases, 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’signore SA4018
using a line directive, updating pets.go
:
// Package pets contains useful functionality for pet ownerspackage petsimport ("fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 directivesfood = foodfmt.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 behinda lot of unused code, as it simplifies the generation process. Instead of manually annotating every instance of unusedcode, 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 ownerspackage petsimport ("fmt")//lint:file-ignore SA4018 trying out file-based linter directivestype Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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 = foodfmt.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 ownerspackage petsimport ("fmt")type Animal intconst (Dog Animal = iotaSnake)type Pet struct {Kind AnimalName 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:
- the “Developer tools as module dependencies” guide guide to see how to add toolslike Staticcheck to a project.
- the Staticcheck documentation for more details about Staticcheck itself.
As a next step you might like to consider:
- Developer tools as module dependencies
- Working with private modules
- Installing Go