//go:fix inline and the source-level inliner

Go 1.26 introduces a new `go fix` subcommand featuring a source-level inliner, which allows developers to automate API migrations and code modernization safely using a simple `//go:fix inline` directive.
//go:fix inline and the source-level inliner
Go 1.26 contains an all-new implementation of the go fix
subcommand, designed to help you keep your Go code up-to-date and modern. For an introduction, start by reading our recent post on the topic. In this post, we’ll look at one particular feature, the source-level inliner.
While go fix
has several bespoke modernizers for specific new language and library features, the source-level inliner is the first fruit of our efforts to provide “self-service” modernizers and analyzers. It enables any package author to express simple API migrations and updates in a straightforward and safe way. We’ll first explain what the source-level inliner is and how you can use it, then we’ll dive into some aspects of the problem and the technology behind it.
Source-level inlining
In 2023, we built an algorithm for source-level inlining of function calls in Go. To “inline” a call means to replace the call by a copy of the body of the called function, substituting arguments for parameters. We call it “source-level” inlining because it durably modifies the source code. By contrast, the inlining algorithm found in a typical compiler, including Go’s, applies a similar transformation, but to the compiler’s ephemeral intermediate representation, to generate more efficient code.
If you’ve ever invoked gopls’ “Inline call” interactive refactoring, you’ve used the source-level inliner. (In VS Code, this code action can be found on the “Source Action…” menu.) The before-and-after screenshots below show the effect of inlining the call to sum
from the function named six
.
The inliner is a crucial building block for a number of source transformation tools. For example, gopls uses it for the “Change signature” and “Remove unused parameter” refactorings because, as we’ll see below, it takes care of many subtle correctness issues that arise when refactoring function calls.
This same inliner is also one of the analyzers in the all-new go fix
command.
In go fix
, it enables self-service API migration and upgrades using a new //go:fix inline
directive comment. Let’s take a look at a few examples of how this works and what it can be used for.
Example: renaming ioutil.ReadFile
In Go 1.16, the ioutil.ReadFile
function, which reads the content of a file, was deprecated in favor of the new os.ReadFile
function. In effect, the function was renamed, though of course Go’s compatibility promise prevents us from ever removing the old name.
package ioutil
import "os"
// ReadFile reads the file named by filename…
// Deprecated: As of Go 1.16, this function simply calls [os.ReadFile].
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
Ideally, we would like to change every Go program in the world to stop using ioutil.ReadFile
and to call os.ReadFile
instead. The inliner can help us do that. First we annotate the old function with //go:fix inline
. This comment tells the tool that any time it sees a call to this function, it should inline the call.
package ioutil
import "os"
// ReadFile reads the file named by filename…
// Deprecated: As of Go 1.16, this function simply calls [os.ReadFile].
//go:fix inline
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
When we run go fix
on a file containing a call to ioutil.ReadFile
, it applies the replacement:
$ go fix -diff ./...
-import "io/ioutil"
+import "os"
- data, err := ioutil.ReadFile("hello.txt")
+ data, err := os.ReadFile("hello.txt")
The call has been inlined, in effect replacing a call to one function by a call to another.
Because the inliner replaces a function call by a copy of the body of
the called function, not by some arbitrary expression, in principle
the transformation should not change the program’s behavior
(barring code that inspects the call stack, of course).
This differs from other tools that allow for arbitrary rewrites,
such as gofmt -r
, which are very powerful but need to be watched closely.
For many years now, our Google colleagues on the teams supporting Java, Kotlin, and C++ have been using source-level inliner tools like this. To date, these tools have eliminated millions of calls to deprecated functions in Google’s code base. Users simply add the directives, and wait. During the night, robots quietly prepare, test, and submit batches of code changes across a monorepo of billions of lines of code. If all goes well, by the morning the old code is no longer in use and can be safely deleted. Go’s inliner is a relative newcomer, but it has already been used to prepare more than 18,000 changelists to Google’s monorepo.
Example: fixing API design flaws
With a little creativity, a variety of migrations can be expressed as inlinings.
Consider this hypothetical oldmath
package:
// Package oldmath is the bad old math package.
package oldmath
// Sub returns x - y.
func Sub(y, x int) int
// Inf returns positive infinity.
func Inf() float64
// Neg returns -x.
func Neg(x int) int
It has several design flaws: the Sub
function declares its parameters in the wrong order; the Inf
function implicitly prefers one of the two infinities; and the Neg
function is redundant with Sub
. Fortunately we have a newmath
package that avoids these mistakes, and we’d like to get users to switch to it. The first step is to implement the old API in terms of the new package and to deprecate the old functions. Then we add inliner directives:
// Package oldmath is the bad old math package.
package oldmath
import "newmath"
// Sub returns x - y.
// Deprecated: the parameter order is confusing.
//go:fix inline
func Sub(y, x int) int {
return newmath.Sub(x, y)
}
// Inf returns positive infinity.
// Deprecated: there are two infinite values; be explicit.
//go:fix inline
func Inf() float64 {
return newmath.Inf(+1)
}
// Neg returns -x.
// Deprecated: this function is unnecessary.
//go:fix inline
func Neg(x int) int {
return newmath.Sub(0, x)
}
Now, when users of oldmath
run the go fix
command on their code, it will replace all calls to the old functions by their new counterparts. By the way, gopls has included inline
in its analyzer suite for some time, so if your editor uses gopls, the moment you add the //go:fix inline
directives you should start seeing a diagnostic at each call site, such as “call of oldmath.Sub
should be inlined”, along with a suggested fix that inlines that particular call.
For example, this old code:
import "oldmath"
var nine = oldmath.Sub(1, 10) // diagnostic: "call to oldmath.Sub should be inlined"
will be transformed to:
import "newmath"
var nine = newmath.Sub(10, 1)
Observe that after the fix, the arguments to Sub
are in the logical order. This is progress! If you’re in luck, the inliner will succeed at removing every call to the functions in oldmath
, perhaps allowing you to delete it as a dependency.
The inline
analyzer works on types and constants too. If our oldmath
package had originally declared a data type for rational numbers and a constant for π, we could use the following forwarding declarations to migrate them to the newmath
package while preserving the behavior of existing code:
package oldmath
//go:fix inline
type Rational = newmath.Rational
//go:fix inline
const Pi = newmath.Pi
Each time the inline
analyzer encounters a reference to oldmath.Rational
or oldmath.Pi
, it will update them to refer instead to newmath
.
Under the hood of the inliner
At a glance, source inlining seems straightforward: just replace the call with the body of the callee function, introduce variables for the function parameters, and bind the call arguments to those variables. But handling all of the complexities and corner cases correctly while producing acceptable results is no small technical challenge: the inliner is about 7,000 li
Source: Hacker News














