What's the best Static Analysis tool for Golang?

GOLANG
9 min read

If you've spent enough time writing in Golang, you've probably written code like this by accident:

// FinalizeSomeWork returns an error on failure, and nil on success.
func (c *Connection) FinalizeSomeWork() error {
	...
    if failed {
        return error
    }
    return nil
}

func void main() {
	conn = &Connection{}
    ...
    // Do some work
	...
    // Work is done, let's commit the changes to our database / filesystem / remote endpoint.
    conn.FinalizeSomeWork()
}

The called function returns an error, but we forgot to check it. The code compiles, and it even appears to work in the common case. But when something goes wrong, we start silently doing the wrong thing, resulting in data loss.

I've personally written this code pattern multiple times, and it's been the source of bugs in Dolt on multiple occasions. It's easy to do this, because the callsite looks right. You need the added context of the function signature to understand that this is wrong.

So, what is the proper way to avoid writing Golang code like this?

One way to avoid these errors is to have tests with good code coverage. Getting 100% coverage requires you to test not just for the common success cases, but the exceptional failure cases as well. For each possible failure condition in your code, you'd write a test that the error is not ignored if that failure condition is met.

But this isn't actually a practical solution, since now instead of needing to remember to check all our errors, we have to remember to write tests that we checked all our errors. We've moved the memory burden, but not eliminated it. It's far better if we don't have to remember, if our environment doesn't let us forget.

That's why a better approach would be to use static analysis to check our code for common pitfalls. For instance, in many languages, silently dropping the return value of a function is a warning. And many languages also have a flag that lets you treat warnings as errors. Perhaps, such an option exists in Go?

Alas, it doesn't. In fact, the Go compiler doesn't have warnings at all, only errors. As the Golang docs explain its philosophy:

If it’s worth complaining about, it’s worth fixing in the code. (Conversely, if it’s not worth fixing, it’s not worth mentioning.)

The Golang devs have decided that since silently dropping a return value may be intended, they won't reject code that does it. And since the compiler won't make you fix it, it won't warn you about it either.

So if the compiler isn't the right place for this check, what is?

I'm choosing to focus on this specific code pattern because it's something that most Go devs are familiar with. But my point is less about any specific code pattern and more about how to prevent any buggy patterns in your code. What I'm really asking is, within the Golang ecosystem, what's the proper way to check for code patterns that are likely bugs, but not guaranteed?

It turns out there is a correct answer. And If you're working with Golang out of the box, even with an IDE, you probably aren't doing it correctly. I know I wasn't.

tl;dr for the impatient: It's this. But we'll get there.

First, lets look at some of the tools that you might reasonably believe would protect you against pitfalls like this, but don't.

golint

golint is the official linter for Golang. Will golint help us prevent errors like this?

Screenshot of golint being deprecated and frozen and having no replacement

No, it won't. Because it's deprecated and has no replacement. Lovely.

Also, it wouldn't have caught this issue anyway, because golint was always more concerned with issues of style than bugginess. It won't warn you about potential bugs in your code, but will happily remind you when to prefer camelCase over snake_case, if that's something that's important to you.

But hey, that README mentioned go vet. Maybe that's the tool that will help us catch these potential errors.

go vet

go vet, like golint, is a static code checker maintained by the Go team and included in the standard Go installation. It's designed to catch code issues that are likely bugs. The very first sentence of their documentation reads:

Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. Vet uses heuristics that do not guarantee all reports are genuine problems, but it can find errors not caught by the compilers.

Reading further, we even see that one of the checks it runs is called unusedresult and has the description check for unused results of calls to some functions. Perfect! This sounds exactly like what we're looking for.

go test even runs go vet on your package-under-test, which sounds awesome... until you realize that you've already been running go test (you have been running go test... right?) and this buggy pattern didn't get flagged by it. But why didn't it? Do we need to set a flag or something? Lets look at the documentation for the unusedresult check:

~ % go tool vet help unusedresult
unusedresult: check for unused results of calls to some functions

Some functions like fmt.Errorf return a result and have no side
effects, so it is always a mistake to discard the result. Other
functions may return an error that must not be ignored, or a cleanup
operation that must be called. This analyzer reports calls to
functions like these when the result of the call is ignored.

The set of functions may be controlled using flags.

This all seems well and good, until we look at the flags.

-unusedresult.funcs value
    	comma-separated list of functions whose results must be used (default context.WithCancel,context.WithDeadline,context.WithTimeout,context.WithValue,errors.New,fmt.Errorf,fmt.Sprint,fmt.Sprintf,sort.Reverse)

Ah, okay, so this check requires you to list every function whose results must be used. You can't use it to detect any unused return value, or even just unused values of a certain type, like error. But why not? What gives?

Turns out, there's been multiple proposals to add this check to go vet. Here's one. Here's another one. Those proposals got rejected, despite an acknowledgement from one of the developers that this check was catching real bugs. Their argument was that there would be too many false positives: it turns out, the Golang standard library has functions like fmt.Println that return errors, but only in certain uncommon conditions. Thus the common practice is to ignore the return value because it will usually be nil, and any static analysis would have to ignore these functions or else be noisy.1

Alright, so go vet won't protect us against this error either. But the golint documentation also mentioned a tool called Staticcheck. Maybe that will help us?

StaticCheck

StaticCheck logo
Tagline
"The advanced Go linter "
Initial Release
2018
GitHub
https://github.com/dominikh/go-tools

StaticCheck is an open source static analyzer for Golang. It doesn't appear to be "official" in the same way that go vet is, but it was started at Google and both Google and the Go team are listed as sponsors, so it might as well be official. It boasts an impressive 150 different checks, including, most crucially:

SA4017 - Discarding the return values of a function without side effects, making the call pointless

Wait, hang on, "a function without side effects"? What about other functions?

Turns out that, just like with go vet, users have requested a check for unused error return values again, and again. And again. And again. The story plays out pretty much the same as with go vet: There's too many places where the standard practice is to ignore errors, mostly in closing file handles and calling standard library functions.

But we're not quite out of leads, because one of those bugs mentioned yet another static analysis tool, a small third-party tool called errcheck.

Do we even dare to hope at this point? If the compiler, golint, go vet, and StaticCheck all decided that this was "out of scope", surely that's because there's another tool where this is in scope, right?

errcheck

Screenshot of errcheck README

Tagline
"errcheck checks that you checked errors."
Initial Release
Feb 2013
GitHub
https://github.com/kisielk/errcheck

Hallelujah. We've finally found a tool that will catch this error for us, only on our fifth try. And it's as simple as installing a pure Golang module and running errcheck ./.... We could easily add that as a CI check if we wanted to.

And yet, this feels like a hollow victory. Does every pitfall have its own separate tool for catching it? Are we expected to search the Internet for each one? Do we still have to remember to run each analyzer individually? Is our workflow now going to involve remembering to run go vet ./... and staticcheck ./... and errcheck ./...., along with any other tools we might later incorporate?

At the end of this journey, have we still only succeeded in replacing one memory burden (checking the return values of all our functions) with another?

Fortunately, the Golang developers anticipated this, and provided a solution.

Go Analysis API

The Go Analysis API provides official interfaces that static analysis tools can implement. These interfaces describe how the analysis passes can be configured, establishes dependencies between passes, and specifies how they report their results. Thus, any analysis driver that implements this API can run any analysis pass that implements this API.

go vet, Staticcheck, and errcheck all support this API. This means that in theory, someone could easily make a single driver program that includes all of these tools and runs them together, in a single action with a single configuration, and reports all failures in one place... and that's exactly what someone did!

Enter golangci-lint.

golangci-lint

golangci-lint logo

Tagline
"Fast linters runner for Go"
Initial Release
May 2018
GitHub
https://github.com/golangci/golangci-lint

golangci-lint is a single binary that comes bundled with all of the most useful Golang static analyzers and runs them all, including every one discussed in this article. It can be run from the command line, but it also has it's own GitHub Action, and plugins for every major IDE.

I actually wasn't aware that golangci-lint existed prior to writing this, and upon adding it to my IDE, I instantly found an actual bug in Dolt. This bug, while not especially serious, was not something that could have been reasonably be caught by unit or integration tests, because it depended on circumstances that were non-obvious. This was enough to convince me of its utility, and its now a mainstay in my IDE. I may even attempt to convince the team to add it to our CI pipeline.

Some of its checks are noisier and less useful than others, but those can be individually disabled or have their severity adjusted. And because of the Go Analysis API, any new static analysis tool can have its checks incorporated into golangci-lint so that every check can be configured from a single interface.

It's not perfect: I'd love to be able to condition go test on a successful run of golangci-lint, but there doesn't seem to be a way to do that. It feels like it should be possible: go test does run go vet by default, and go vet has a -vettool flag that allows running other implementers of the Analysis API. But there doesn't seem to be a way to set the -vettool flag when invoking go test. If I'm missing something, please message me on Discord or email and let me know.

I also wish that there was a bit more official guidance from the language maintainers here. I understand their philosophy that this kind of static analysis is outside the scope of the compiler and other official language toolings, and I appreciate that the Analysis API exists at all, but I definitely would have appreciated an officially endorsed repository of analysis passes, or at least an official tool for bundling analysis passes instead of needing to rely on golangci-lint. This feels like the kind of problem that should have official, well-documented solutions, not a trail of breadcrumbs leading to third-party implementations.

But having gone on this journey, I'm satisfied with the results. And I hope that this article can be a useful map for anyone who comes after.

In summary, if you want good static analysis and linting of Golang code, use golangci-lint(https://golangci-lint.run/).

Thanks for reading. Here at DoltHub, we're shaping the future of the Internet by making the world's first version-controlled SQL database. And it's written 100% in Go, which is why I wrote this: we want to use every tool at our disposal to make Dolt the best it can be.

Dolt is Git for data, meaning it's great for both decentralized data sharing and private collaboration. Whatever your use case is, we think your database can be improved. If you share our vision, come hang out in our Discord server or follow us on Twitter. If you think Dolt might be right for you, we're happy to help you get started.


  1. (Side note to anyone designing a library: DO NOT DO THIS. Do not condition your users to ignore an error return value, because that just guarantees that they'll ignore it in the cases where it actually matters. If the failure condition can be handled by something higher on the call stack, return an error and require that the caller handle or propagate the error. If nothing in the call stack can handle the error, panic.)

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.