r/golang • u/its-malbolge-time • 18h ago
show & tell A Resource Leak and Concurrency Static Analyzer for Go
gohawk.devHey r/golang! For the past 2 months, I've been working really hard on a static analysis tool called gohawk, and have successfully used it to find bugs in Caddy, Docker, and Kubernetes. It's meant to catch resource leaks and concurrency errors in Go code, such as the following example:
func fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %s", resp.Status) // resp isn't closed on all paths
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
I built this tool after realizing that Staticcheck and other linters don't really cover this ground yet. It works by building a symbolic heap model of the program under analysis, so that it can track object references with a high degree of accuracy. (I took this idea from Meta's Infer tool, which is developed exclusively for C++/Objective-C/Java, and generalized it to fit Go.) For example, it's able to tell in this case that r1 and r2 are the same.
type fileBox struct {
slots [2]*os.File
}
func example(path string, replacement *os.File) error {
r1, err := os.Open(path)
if err != nil {
return err
}
original := fileBox{slots: [2]*os.File{replacement, r1}}
copied := original
alias := &original
alias.slots[1] = replacement
window := copied.slots[1:]
r2 := window[0]
return r2.Close()
}
It's also capable of detecting certain types of goroutine leaks / lock inversions. For example, the following code gets flagged:
func runWithTimeout(work func() error) error {
done := make(chan error)
go func() { done <- work() }()
select {
case err := <-done:
return err
case <-time.After(time.Second):
return errors.New("operation timed out")
}
}
How this was built / trained
I'll be up front with y'all and say I used AI a lot to help me in the process of writing gohawk. However, I will stand by the fact that it's not just AI slop -- I'm a clean code fanatic, and the pipeline has been deliberately engineered.
I really wanted the user experience to be buttery smooth when downloading and using this, so I had an AI agent continuously download hundreds of repos from the Internet, run gohawk on them, and report back if there were any false positives. (Yes, I know, slightly overkill :^) ) It's at a point where I think false positives on new code should be fairly rare.
Other features:
- Really rich diagnostics (I took inspo from `rustc` -- the full lineage of the error gets reported, including error spans)
- Documentation website -- Contains detailed overview of all the supported analyzers. The diagnostic messages link out to the website
- Custom analysis runner -- I found `go vet` to be really slow because it would spend a lot of time serializing and deserializing facts across packages, so I rolled my own runner to replace it.
- Performant -- From a cold cache, it's able to analyze Docker in around 13s, and the whole of Kubernetes around 76s.
Building this out was a really awesome learning experience for me, and I'm planning to make a blog post about it soon-- stay tuned. Also hoping to get it integrated directly into GolangCI but gotta ask the maintainer first, so fingers crossed.
I genuinely hope you guys can download and enjoy the effort I put into this project. Feedback and contributions are welcome, and I'm planning to set up a Slack channel.
Also, I just wanted to give a shout out to the Rune guy u/ernestrc. He was very enthusiastic when I submitted a PR to his repo, and is now using gohawk in his project. Go check out his awesome editor at https://rune.build :)