CLI tool
uber-go/nilaway avatar
uber-go/nilaway

NilAway: cross-package nil panic detection for Go, and where it stops

Static analysis tool to detect potential nil panics in Go code. NilAway [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [!WARNING] NilAway is currently under active development: false positives and breaking changes can happen.

3,904 stars96 forksGoApache-2.0

At a glance

What is it?
NilAway is Uber's go/analysis-based linter that tracks nil flows across package boundaries without annotations. It installs in one go install command, but the README warns of false positives and breaking changes while the project is under active development.
Who is it for?
Adopt NilAway if you maintain a Go codebase with a package prefix you can pass to include-pkgs and you accept that the README calls the project under active development with false positives and breaking changes possible. Skip it if you cannot tolerate non-actionable findings in third-party code and will not configure include-pkgs, or if you only want a single-binary linter with no plugin build step.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 14, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The nil panic problem NilAway targets across package boundaries

A nil dereference in Go is a runtime failure, and the compiler will not tell you about it. The standard library ships a nilness analyzer, and NilAway's README positions itself against it directly: NilAway is "similar to the standard nilness analyzer, however, it employs much more sophisticated and powerful static analysis techniques to track nil flows within a package as well across packages." That word across is the whole pitch. Most nil bugs that reach production come from a function in one package returning a nil pointer that a caller in another package dereferences without a check, and single-package analysis cannot see the return contract. NilAway's stated audience is developers who want those failures caught at compile time rather than runtime. The README also describes an inference engine that requires no annotations beyond standard Go code, which matters because annotation-based contracts only work if every contributor writes them. The project is Apache-2.0 licensed and lives at go.uber.org/nilaway. One caveat sits at the top of the README in a warning block: NilAway is currently under active development, and false positives and breaking changes can happen.

How NilAway tracks nil flows without annotations

NilAway is implemented with the go/analysis framework, the same interface that golangci-lint, nogo and the standalone singlechecker driver consume. That choice determines both how you run it and how fast it is. Because the analysis is expensive, NilAway caches its findings about a particular package through the Fact Mechanism from go/analysis. Facts are the framework's way of letting one analyzer publish conclusions that another analyzer invocation can read later, which is how cross-package reasoning survives incremental builds. The README is explicit that this makes the choice of driver a performance decision: a driver with modular analysis support, meaning bazel/nogo or golangci-lint, stores facts in a way that persists, while the standalone checker keeps all facts in memory. The README calls the standalone checker an evaluation tool for that reason. There is a second design consequence. By default NilAway analyzes all Go code, including the standard library and your dependencies, which the README says helps it understand dependency code and reduce false negatives, at the cost of significant performance overhead and a larger number of non-actionable errors in dependencies. The include-pkgs flag is the escape hatch, and the README recommends it for large projects.

Installing NilAway and running a first analysis

The quickest path is the standalone binary, installed from source. The README gives this exact command:

bash
go install go.uber.org/nilaway/cmd/nilaway@latest

After that, the binary is on your path and you run it against a package pattern. The README's example narrows analysis to your own code with include-pkgs, which takes a comma-separated list of package prefixes:

bash
nilaway -include-pkgs="<YOUR_PKG_PREFIX>,<YOUR_PKG_PREFIX_2>" ./...

Replace the placeholders with your module's import path prefixes. What you should see is a list of reported nil flows; the README describes the errors as providing the nilness flows for easier debugging, meaning the report includes the path the nil value travelled, not just the dereference site. If you want machine-readable output, the README notes that pretty-printing must be turned off for JSON:

bash
nilaway -json -pretty-print=false -include-pkgs="<YOUR_PKG_PREFIX>,<YOUR_PKG_PREFIX_2>" ./...

For a real project, the README steers you to golangci-lint instead, and only the Module Plugin System is supported. Start with a .custom-gcl.yml at the repository root:

yaml
version: v1.57.0
plugins:
  - module: "go.uber.org/nilaway"
    import: "go.uber.org/nilaway/cmd/gclplugin"
    version: latest # Or a fixed version for reproducible builds.

Then enable it in .golangci.yaml. For golangci-lint v2 the README shows this shape, where the settings map keys are flag names:

yaml
version: "2"
linters:
  enable:
    - nilaway
  settings:
    custom:
      nilaway:
        type: module
        description: Static analysis tool to detect potential nil panics in Go code.
        settings:
          include-pkgs: "<YOUR_PACKAGE_PREFIXES>"

Finally build and run the custom binary. The README notes the bootstrapping golangci-lint must itself be at least v1.57.0, and that the output binary defaults to ./custom-gcl:

bash
golangci-lint custom
./custom-gcl run ./...

The README suggests caching that binary, using the hash of .custom-gcl.yml as the cache key when the NilAway version is fixed, or appending the build date when the version is latest.

False positives, dependency noise and the in-memory fact store

The most concrete limitation is stated by the project itself. NilAway cannot be merged into golangci-lint as a bundled linter in its current form, and the README says the reason is that it can report false positives, pointing to PR#4045. That is why you must build a custom binary through the module plugin system rather than adding a line to your linter list. Treat the false positive rate as an unknown you have to measure on your own code, because the README does not publish one. The second limitation is dependency noise. Analyzing all code by default produces errors in third-party packages that you may not be able to fix, and the README's own recommendation is to use include-pkgs to skip them, which trades away some of the cross-package insight that makes NilAway interesting. The third is driver-dependent performance. The Fact Mechanism makes results cheap to reuse on modular drivers, but the standalone checker holds all facts in memory, so the README explicitly says it is for evaluation. If you plan to run NilAway in CI on a large monorepo, the standalone binary is the wrong tool and so is any driver without modular fact storage. The README also warns that breaking changes can happen while the project is under active development, which matters if you pin the plugin to latest.

Where NilAway sits next to the standard nilness analyzer and Staticcheck

The natural comparison is the analyzer NilAway names in its own README: the standard nilness analyzer at golang.org/x/tools/go/analysis/passes/nilness. The difference is scope of reasoning. The standard analyzer works within a package; NilAway's stated advantage is tracking nil flows across packages, which is where the return-value-contract bugs live. If your nil panics all originate inside single functions, the standard analyzer is cheaper and already in your toolchain. Staticcheck is the other reference point engineers reach for, and it is a broad linter covering correctness, performance and style checks rather than a nil-flow inference engine, so the two are complements rather than substitutes. On the driver side, nogo under Bazel is the alternative to golangci-lint, and the README treats both as modular drivers suitable for large projects, noting that bazel/nogo requires more setup: you first configure rules_go, gazelle and nogo so the project builds with Bazel, then add the NilAway import. The trade-off is real. golangci-lint gets you a custom binary with one build command; Bazel gets you fact caching inside a build system you already have to run. Neither avoids the false positives.

Maintenance, upgrades and the Apache-2.0 licence

The repository is not archived. The README carries a warning that NilAway is currently under active development, and that false positives and breaking changes can happen. The available facts do not include the date of the last push, so there is no way to state how recently the code changed; the warning block is the only maintenance signal available. Upgrade cost is shaped by the plugin setup. The Makefile exposes an upgrade-deps target that checks the latest golangci-lint release through the GitHub API, compares it with the version recorded in .golangci.version, and rewrites that file when they differ, then upgrades dependencies across the module directories, which include ./tools and ./testdata/integration. That is a maintainer-facing target, not something an adopter runs. On the adopter side, the README's own advice is to pin the plugin version for reproducible builds, or cache the custom binary keyed on the .custom-gcl.yml hash if you track latest. The module requires Go 1.25.0 per go.mod, so older toolchains cannot build it. NilAway is Apache-2.0 licensed, with a NOTICE file present in the repository root; that is a permissive licence, but this is not legal advice and you should read the LICENSE and NOTICE files yourself before redistributing a modified build.

Editorial conclusion

Adopt NilAway if you maintain a Go codebase with a package prefix you can pass to include-pkgs and you accept that the README calls the project under active development with false positives and breaking changes possible. Skip it if you cannot tolerate non-actionable findings in third-party code and will not configure include-pkgs, or if you only want a single-binary linter with no plugin build step. Before rolling it out, verify three things: that your golangci-lint is at least v1.57.0, that the .custom-gcl.yml plugin entry pins a version rather than latest if you need reproducible builds, and that the include-pkgs value matches your module path prefix exactly. The standalone binary is the fastest way to see what NilAway reports on your tree, but the README states it stores all facts in memory, so treat it as an evaluation tool rather than the long-term driver.

Frequently asked questions

What does "nil" mean in Go?

Nil is the zero value for pointer, interface, map, slice, channel and function types, and dereferencing or calling through a nil value panics at runtime. NilAway exists to catch those dereferences at compile time instead, tracking where a nil value can flow before it is used.

How do I run NilAway with golangci-lint?

NilAway is not bundled as a golangci-lint linter, so you build it as a module plugin. Create a .custom-gcl.yml with the module go.uber.org/nilaway and the import go.uber.org/nilaway/cmd/gclplugin, enable nilaway under linters in .golangci.yaml, then run golangci-lint custom and execute the resulting ./custom-gcl binary. The bootstrapping golangci-lint must be at least v1.57.0.

Why does NilAway report errors in my dependencies?

By default NilAway analyzes all Go code, including the standard library and third-party dependencies, which the README says reduces false negatives but produces non-actionable errors in dependency code. Passing the include-pkgs flag with your own package prefixes restricts the analysis to first-party code.

Can I use the standalone nilaway binary in CI?

The README recommends against it for large projects. NilAway caches findings through the go/analysis Fact Mechanism, and the standalone checker stores all facts in memory, so the README describes it as provided for evaluation purposes and points to bazel/nogo or golangci-lint for modular analysis support.

Official sources

  1. Official README
  2. Project repository
Community notes

Community notes