resourcelifetime: Detect resource leaks in GoSkip to content
Blog
Resources and lifecycle

resourcelifetime

Checks owned files, SQL handles, HTTP responses, and compressors are released on every path.

Tiercore
Kindsdefect, hazard

What it detects

Reports files, SQL rows, statements and transactions, HTTP response bodies, and gzip/zlib writers that are not released on every return path, and operations on them after release.

Some cases are deliberately not reported:

  • a resource returned, stored somewhere that outlives the function, or handed to code gohawk cannot see into;
  • channel timers and tickers, which the garbage collector reclaims;
  • a file, response body, or rows value opened once in main.main, which program exit closes.

The full list of precision boundaries, with the fixtures that pin each one, is in the resourcelifetime design notes.

Checks

Check Tier Kind What it detects
missing-release
core defect Reports owned resources that are not released on every return path.
use-after-release
core hazard Reports an invalidating operation on the same resource after a dominating release, with no intervening unknown effects.

Why this is flagged

Owned resources are limited and often hold work open elsewhere. Missing cleanup on even one return path can leak file descriptors, database connections, network bodies or transactions until the program slows down or fails.

Further reading: Effective Go: Defer, Package net/http.

How to fix it

As soon as a resource is acquired successfully, arrange for its matching cleanup on every later return path. Use defer when the current function owns the resource, or transfer it only when the receiver clearly takes ownership.

Examples

Flagged code

Leaked file

func read(path string) error {
// gohawk: owned resource from os.Open is not released on every return path
file, err := os.Open(path)
if err != nil {
return err
}
_ = file
return nil
}

Leaked database rows

func query(ctx context.Context, database *sql.DB) error {
// gohawk: owned resource from sql.QueryContext is not released on every return path
rows, err := database.QueryContext(ctx, "SELECT 1")
if err != nil {
return err
}
_ = rows
return nil
}

Accepted code

func readSafely(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
return nil
}