Go 1.26’s Source-Level Inliner — Self-Service API Migration at Scale

1. Introduction
Every Go developer has seen the deprecation notice: “Deprecated: use os.ReadFile instead.” But actually migrating all those ioutil.ReadFile calls across a large codebase has historically been a manual, error-prone process.
Go 1.26 solves this with a redesigned go fix command and a new //go:fix inline directive. Together, they enable package authors to embed migration logic directly in their code, letting users run a single command to automatically rewrite deprecated API calls to their modern equivalents.
In this article, we’ll explore how the source-level inliner works, the six technical challenges it solves, and what this means for Go API evolution.
2. The Problem: Deprecation Without Migration
Before Go 1.26, deprecating an API in Go looked like this:
1 | // Deprecated: Use os.ReadFile instead. |
Users saw the deprecation notice in their IDE, but migrating required:
- Manually finding all call sites (grep, IDE search)
- Rewriting each one by hand
- Potentially adjusting import statements
- Reviewing the diff
For large codebases or popular packages, this is an enormous amount of friction. The result? Deprecated APIs persist for years, and libraries hesitate to make breaking changes.
3. The Solution: //go:fix inline
Go 1.26 introduces the //go:fix inline directive. You place it on a function that is a thin wrapper, and go fix will automatically replace calls to that function with its body.
Here’s the basic usage:
1 | package ioutil |
Now, running go fix -fix=inline ./... on any codebase that imports ioutil will automatically rewrite:
1 | // Before |
Note that go fix also handles import cleanup — removing ioutil from imports and adding os if not already present.
4. Six Technical Challenges
The implementation sounds simple, but correctly inlining a function call is surprisingly complex. The Go team documented six challenges they had to solve:
Challenge 1: Parameter Elimination
When a parameter appears multiple times in the function body, naively substituting the argument would evaluate it multiple times:
1 | //go:fix inline |
The inliner detects multi-use parameters and inserts explicit bindings to ensure single evaluation.
Challenge 2: Side Effects
Function call arguments are evaluated left-to-right. Reordering them during inlining can change observable behavior:
1 | //go:fix inline |
The inliner analyzes whether safe reordering is possible, falling back to parameter bindings when it cannot be proven safe.
Challenge 3: Fallible Constant Expressions
Some substitutions would create compile-time errors that didn’t exist before:
1 | //go:fix inline |
The inliner avoids creating constant expressions that could fail at compile time, preserving runtime semantics.
Challenge 4: Shadowing
Identifiers in the function body must refer to the same symbols after inlining:
1 | package pkg |
When inlined into a caller that has a local os variable, os.ReadFile would resolve incorrectly. The inliner inserts bindings to preserve the original namespace.
Challenge 5: Unused Variables
Go’s compiler rejects unused variables. A naive inlining might create variables that are never used in the caller’s context:
1 | //go:fix inline |
The inliner tracks the last reference to each local variable and generates code that prevents “declared and not used” errors.
Challenge 6: Defer Statements
Functions containing defer cannot be straightforwardly inlined because defer semantics are tied to function scope:
1 | //go:fix inline |
For these cases, the inliner wraps the body in an immediately-invoked function literal:
1 | // Inlined as: |
5. Real-World Impact
The Go team has already used this mechanism to prepare over 18,000 changelists for automated code modernization in Google’s internal monorepo. This represents the practical scale at which this feature operates.
For the broader Go ecosystem, this changes how library authors can think about API evolution:
1 | // v1 API (deprecated) |
Users just run go fix and get a correct migration, even when parameter semantics changed.
6. Using It Today
To apply inline fixes in your project:
1 | # Fix all inline-annotated deprecations in current module |
To mark your own deprecated functions for auto-migration:
1 | // YourPackage/compat.go |
Note that //go:fix inline only works on functions that are pure wrappers — their entire body must be a single return statement calling the replacement.
7. Conclusion
The //go:fix inline directive and the redesigned go fix command represent a significant step forward in how Go handles API evolution. Package authors can now embed migration paths directly in their code, and users can execute those migrations automatically. It shifts the burden of deprecation from “every user manually migrates” to “the package author writes the migration once.”
The six technical challenges the team solved — side effects, shadowing, defer, unused variables, fallible constants, and multi-use parameters — show just how much correctness work goes into what looks like a simple text substitution.
Have you ever manually migrated ioutil calls across a large codebase? Would //go:fix inline have saved you hours of work? Share your thoughts!
More in the “You Should Know In Golang” series:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
Comments