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:
- the “Developer tools as module dependencies” guide guide to see how to add tools like Staticcheck to a project.
- the Staticcheck documentation for more details about Staticcheck itself.
As a next step you might like to consider: