Calling a function is not free. The caller sets up arguments, jumps, and the callee eventually returns — a fixed cost paid on every call regardless of what the function does.
Inlining removes it by pasting the function body into the call site. No jump, no return, and the optimiser gets to see the caller and callee as one piece of code, which usually matters more than the call overhead itself.
The interesting question is not whether it helps. It is when the saving is large enough to notice.
How Go decides
Unlike C++, Go has no inline keyword. The compiler decides, using a cost budget: it walks the function body, assigns a cost to each node, and inlines if the total falls under a threshold. Small leaf functions qualify; large ones and (historically) anything with certain control flow do not.
You can see the decisions:
go build -gcflags="-m" ./..../main.go:5:6: can inline AddInlined
./main.go:12:20: inlining call to AddInlinedThis is the part worth knowing, because it turns a guess into an observation. -m -m gives the reasoning, including why something was rejected.
To force the issue in the other direction, //go:noinline on a function excludes it. That is a benchmarking and debugging tool, not something to ship.
A benchmark where it matters
Two identical functions, one excluded from inlining:
//go:noinline
func AddNonInlined(a, b int) int {
return a + b
}
func AddInlined(a, b int) int {
return a + b
}goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddNonInlined-12 970689716 1.234 ns/op
BenchmarkAddInlined-12 1000000000 0.2503 ns/opRoughly five times faster. The function body is a single addition — about one cycle — so the call overhead is not a tax on the work, it is the work.
NOTE
Be suspicious of your own microbenchmarks at this scale. 0.25 ns/op is under one cycle on a 2.6 GHz machine, which is a strong hint the optimiser eliminated part of the loop once it could see through the call. That is a real effect of inlining, but it means the number measures "code the compiler deleted" as much as "call overhead removed".
At sub-nanosecond resolution, use -gcflags="-m" to confirm what was inlined and read the assembly if the answer matters.
A benchmark where it does not
Give both functions real work to do:
//go:noinline
func AddNonInlined(a, b int) int {
sum := a + b
for i := 0; i < 100000; i++ {
sum += i
}
return sum
}BenchmarkAddNonInlined-12 43389 26175 ns/op
BenchmarkAddInlined-12 48938 23937 ns/opThe gap has essentially vanished, and the arithmetic explains why. Call overhead is around one nanosecond. The body now costs about 26,000 nanoseconds. The overhead is roughly 0.004% of the runtime — well inside the noise of two benchmark runs.
Nothing about inlining got worse. The fixed cost is exactly what it was; it is just no longer a meaningful fraction of the total.
This is the whole story of inlining in one comparison. It removes a constant, so it matters in inverse proportion to what the function costs.
What this means in practice
Inlining pays for small functions called often. Getters, comparators, the tiny helper inside a hot loop. Those are also the functions Go's inliner is most likely to handle without being asked.
It does not pay for functions that do real work, and it costs something: more code means more instruction-cache pressure, and a larger binary. Compilers weigh this, which is why the budget exists.
The practical advice is narrow, because there is not much to do:
- Profile before caring. If inlining shows up as your bottleneck, you have already fixed the real problems.
- Keep hot-path helpers small if you want them inlined. That is the one lever you actually control — the budget is about function size, so a function that does one thing stays eligible.
- Check with
-gcflags="-m"rather than assuming. - Do not restructure code for it. Hand-inlining hurts readability and duplicates logic to buy a nanosecond you are unlikely to be able to measure.
The Go compiler optimizations wiki (opens in a new tab) documents the current rules, which do change between releases.
The general shape
Inlining belongs to a family of optimisations that trade space for time by removing indirection. It is close to free when the thing being removed is comparable in size to the thing doing the removing, and pointless when it is not.
Recognising which case you are in is a matter of comparing two numbers: the fixed cost, and the cost of the work. The second benchmark above is not a demonstration that inlining fails. It is a demonstration of arithmetic.