Interprocedural Analysis: Catch nil Dereferences Before They Crash Your Code
The upcoming GoLand 2025.2 release introduces a powerful set of new features and improvements designed to help you write safer, more reliable Go code. If you’d like a full breakdown of all the updates, be sure to check out the release notes.
In this post, we’ll focus on one of the most significant new features: interprocedural code analysis for detecting nil
pointer dereferences. By helping you catch subtle bugs that often slip through code reviews and tests, this improvement makes your production code more stable and easier to maintain.
The GoLand team has put a lot of effort into delivering deeper, smarter static analysis to improve your development experience and help prevent those frustrating runtime panics. If you want to try this feature in your IDE, you can clone the following project from GitHub.
nil
pointer dereference in Go
One of the most common pain points in the Go programming language is nil
pointer dereference, and nearly every Go developer has encountered it at some point. Despite Go’s simplicity and strong static typing, nil
remains a source of subtle and often critical bugs.
The impact of a nil
dereference can be severe, especially in production environments. A single unexpected dereference can crash an entire service, bringing down an API or worker process with little to no warning.
In Go, even more subtle issues can arise. Writing to a nil
channel, for instance, can cause a goroutine to be blocked forever, potentially leading to deadlocks and cascading system failures. Attempting to access fields on an uninitialized nil
pointer will result in an immediate panic. These kinds of errors are easy to overlook and hard to trace back once deployed.
While some nil
dereference issues can be caught through careful code review or testing, that’s not always enough. In fast-paced development cycles or large codebases, it’s easy for subtle nil
-related bugs to slip through. Ideally, such issues should be detected automatically and as early as possible when writing the code.
This is where static code analysis comes in. GoLand already includes a built-in nil
dereference inspection that performs local intraprocedural analysis. It works well for many common scenarios, detecting when a pointer might be nil
within the scope of a single function.
However, the current analysis only works within individual functions. It does not follow how values move between functions, so it can miss problems that involve multiple calls. These more complex cases are common in real-world Go code and are often the most dangerous. To catch them, we’ve implemented something more powerful: interprocedural code analysis.
Interprocedural code analysis
Interprocedural analysis, also called global analysis, helps you understand how values move through function calls. It looks beyond a single function to follow data across files and packages. In contrast, intraprocedural or local analysis only checks what happens inside one function. Local problems are often easy to catch by reviewing a single function. But global problems are harder to find because the source of an issue, such as a nil
value, might be far from where it causes an error. That is why interprocedural analysis is especially useful for detecting nil
dereference issues.
Following the flow: Understanding nil
dereferences
Now let’s take a look at an example. This code looks pretty straightforward. We create a user using a constructor and print its fields. But the analysis gives us a warning: user.Age
might cause a nil
dereference.

Let’s try to investigate this manually. To understand what’s going on, we need to look at how the NewUser
function is implemented. It is defined in a different file called model.go
.

This constructor looks a bit strange: NewUser
returns nil
if an error occurs, but in main
, we use the result without checking. This creates a potential nil
dereference.
To fix this, we can rewrite NewUser
to return both a result and an error – the more idiomatic Go style.

Now the code is safer. We check for an error before accessing user
, so there is no risk of dereferencing nil
. Although this code looks correct, we still see the same warning.
To figure out what’s going on, let’s dig in deeper and take a closer look at the implementation of CreateUser
.

Here we find the second cause of the problem.
In the CreateUser
function, there is a case where the code returns nil
for both the user
and the error
.

This is a fairly common mistake in error handling. Returning nil
without an error makes it look like everything went fine, while in reality the result is not valid. The caller checks only the error, sees that it is nil
, and then tries to use the result. In our example, this leads to a crash when the code accesses user.Age
.
We can fix this by returning an actual error when the input is not valid:

With this change, the code becomes correct, and the inspection no longer reports a nil dereference.
Finding issues like this by hand can be slow and frustrating, especially in large projects. The place where a nil
value is created might be far from where it causes a problem.
That is why GoLand highlights such issues right in the editor as soon as they are detected. For these warnings, we offer a dedicated context action: Explain potential nil dereference. This action opens the Data Flow Analysis tool window, where you get a step-by-step explanation of how the nil
value flows through the code and where it is eventually used. This makes it much easier to understand and fix the issue without searching through the entire codebase.
When nil
slips through: Catching unsafe arguments and receivers
Our analysis does more than track return
values. It can also reason about parameter nilability by understanding whether a function expects a non-nil argument or can safely accept nil
. This is particularly useful for catching cases where a nil
value is unintentionally passed to a function that doesn’t handle it properly.
Let’s take a look at another example:

Here we call the Copy
method on a user
. At the same time, we pass nil
as the context, assuming it is safe to do so.
But the inspection shows a warning: The context argument might cause a nil
dereference as we pass a nil
value as the context. Let’s check the implementation of the Copy
method:

In this code, the method accesses ctx.isDebugEnabled
without checking whether ctx
is nil
. If ctx
is nil
, the program will panic at runtime.
To fix this, we can make the ctx
parameter nil-safe by adding an explicit nil
check before accessing its fields.

With this change, the code becomes safe, and the warning at the call site disappears.

However, that is not the only problem. The analysis also reports a potential nil
dereference related to the user
variable.
To understand why, we can use the Explain potential nil dereference action.
The process
function allows user
to be nil
, and we pass it to Copy
without checking.
Inside the Copy
method, the receiver u
is used before being checked. Specifically, u
is passed to the logUserEvent
function, where a dereference occurs when accessing the u.Name
field. Therefore, if the user
variable in the process
function is nil
, a nil
dereference will occur.
These examples demonstrate that nil
dereference issues are often subtle and easy to overlook. Even if the code looks clean and idiomatic, small assumptions can lead to runtime crashes. Tracing the root cause manually can be surprisingly tricky, especially when the origin of the nil
value is created far from the place where it is used, separated from the dereference by multiple function calls, files, or packages.
This is where interprocedural analysis helps. It tracks how nil
values move through function calls. Instead of guessing where the problem started, you can clearly see the full path from the origin to the point of dereference.
Quick documentation now shows nilability information
Nilability analysis in GoLand is not just for highlighting issues in the editor. As you’ve already seen, our analysis can determine whether a function might return nil
, and whether it’s safe to pass nil
as an argument to a particular parameter. As the analysis understands how functions are expected to behave, we decided to make this information easy to access. That’s why we’ve integrated nilability information directly into the quick documentation popup.
Let’s go back to the first example from earlier, before we applied any fixes. If we place the caret on the NewUser
function and trigger the quick documentation, we will see a section called Nilability info. It shows the nilability of the function parameters and the return
value. In this example, the function may return a nil
result, and the quick documentation popup tells us that clearly.

The same feature works for parameters and receivers. In the second example, also before applying any fixes, the Nilability info section shows us that both the receiver u
and the parameter ctx
of the function are expected to be non-nil.

This small addition makes a big difference. With a quick lookup, you get an overview of important details, which can help you write safer code and reduce the risk of unexpected nil
dereferences. However, keep in mind that not all cases are covered by the analysis, so always review the code carefully.
Limitations and trade-offs
The first version of this analysis is simple and careful on purpose. It does not try to catch every possible nil
dereference, and that is intentional. We’ve focused on the most common and important cases, aiming to keep false positives to a minimum. We’ll keep improving the analysis over time, adding new cases carefully. Our goal is to catch more problems without adding unnecessary noise.
Avoid panics; embrace safety
Interprocedural code analysis makes it much easier to catch and fix nil
pointer dereference issues early. By tracking nil
values across functions, files, and packages, this analysis makes it easier to understand the root causes of potential bugs before they hit production, reducing downtime and preventing costly incidents.
We’re excited to continue refining and expanding these capabilities in future updates. Stay tuned – and as always, we’d love to hear your feedback!
The GoLand Team