Go 1.27 ships. I was wrong about PGO. I'm still not wrong about iterators.
Published by RodHat

Go 1.27 dropped today. This is the August release — Go ships on a six-month cadence, February and August, has done so since 1.21, will probably do so until the heat death of whatever data center hosts the release infrastructure. The clock is reliable. What ships on the clock varies.
This cycle, two things are worth your time.
PGO. I owe it an apology.
Profile-guided optimization landed in Go 1.21 as generally available, August
2023. My reaction at the time was approximately: “compilers have been doing PGO
since the 1990s, IBM’s XL compilers on AIX did it, GCC got -fprofile-generate
in 2003, the fact that Go is treating this as a feature in 2023 tells you
something about where Go was starting from.”
I stand by that as history. I’m retracting it as a current assessment.
I’ve had three years to watch Go PGO actually run on real workloads, and the
numbers in 1.27 are not vaporware. On a plain HTTP proxy I maintain on my lab
network — nothing exotic, reads requests, rewrites headers, forwards upstream —
I’m seeing 14% higher throughput against 1.25 after adding a PGO profile. No
code changes. The profile came from a five-minute pprof CPU trace under real
load. Feed the profile to go build -pgo=profile.pprof, binary comes out the
other side faster.
The Go team is reporting 12-18% gains across representative workloads. That range is real. CPU-bound work hits the high end. I/O-bound work hits the low end. The HTTP proxy lands in the middle of the range, which is about what you’d expect.
The mechanism is unsurprising if you know your compilers: PGO identifies hot call paths in the profile data and makes better inlining decisions. Go’s inliner is conservative by default — it has to be, because Go compilation speed is a first-class priority and aggressive inlining explodes compile time. PGO gives the inliner permission to be less conservative on paths that actually matter, without paying the cost everywhere.
If you run a Go service in production and you’ve never profiled it: that’s two
problems. Fix the profiling problem first, because you need production traces
anyway. Then feed them to go build. You get the gains for free. If you want a
refresher on what perf and CPU profiling look
like at the system level, that’s a reasonable starting point before you go hunting
through the pprof docs.
The PGO workflow in 1.27 is dead simple:
# collect a CPU profile from production (pprof format)
# then:
go build -pgo=cpu.pprof -o myservice ./cmd/myservice
That’s it. No rebuild flags, no profiling server to stand up, no profile registry to maintain. The binary embeds what it needs. For a language that couldn’t ship a package manager until 1.11, this is correctly minimal.
Iterators. I’m not retracting anything. But I’m done arguing.
Go’s range-over-func landed as experimental in 1.23 (August 2024). The iter
package gave you iter.Seq[V] and iter.Seq2[K,V] — which are just function
types with a specific signature — and a handful of adapters. The standard library
got slices.All, slices.Values, maps.All, maps.Keys, maps.Values.
My reaction was: “this is a function that takes another function and calls it in a loop, wrapped in three layers of type aliases and sold as a feature.” I wrote something similar in a note at the time. I am not retracting it because it is accurate.
What I am retracting is the implied conclusion: that this was therefore bad.
By 1.27, the iter pattern is everywhere in the standard library. The
new streaming path in encoding/json accepts iterators. The database/sql query
results can be ranged over. The standard library team has been consistent about the
design: push iterators (your function calls yield, not the other way around) because
they compose with defer and recover correctly, which pull iterators can’t guarantee.
That design decision is correct. I know it’s correct because I’ve hit the exact
failure mode it avoids: you write a pull iterator for database rows, someone
wraps it in a goroutine, the goroutine leaks when the caller panics before
Close(), the connection pool drains, the on-call engineer gets a 3am page.
Push iterators with range eliminate that class of bug because the range loop
owns the cleanup, not the caller.
So: the implementation looks like a function that takes a function that takes a value and returns a bool. It does look like that. That’s fine. Write it once, range over it everywhere, defer cleanup correctly, move on with your life.
The stuff that doesn’t get a section but deserves a mention
The linker is faster again. Go’s build times have been a steady improvement
story since 1.16. 1.27 continues that. Nothing dramatic, but go build ./... on
a medium-sized codebase that used to take 8 seconds now takes 6. Cumulative gains
matter.
Weak pointers landed in 1.24 and are now in production everywhere. unique.Handle
for interned strings, runtime.AddCleanup as the finalizer that actually works
correctly. If you’re still using sync.Pool for string interning in 2026, look at
what the unique package does first — it’s the right tool for most of those cases.
go test -count=1 -shuffle=on should be your default. This has been available
for years and is newly emphasized in the 1.27 release post. If your tests pass
in order but fail shuffled, your tests are wrong, not your code. Run shuffled,
fix the coupling.
The thing I actually want to say
Go’s toolchain is still the best in the business for the class of work it targets. Single binary. No runtime. Build from source in one command on any machine with a Go toolchain installed. The binary runs on the machine you’re targeting without an interpreter, a JVM, a virtualenv, a Gemfile, or a Docker container that weighs six hundred megabytes and includes a full Debian install so your four-hundred-line HTTP service can boot.
I do not write much Go. I am a C and shell person by formation and a FreeBSD person by conviction. But when someone hands me a Go codebase, I am not annoyed about it the way I am annoyed by what gets handed to me in other languages. That is the highest compliment I can give a language I didn’t design.
If you run Go in production: update to 1.27, add PGO profiles to your build pipeline, start using range-over-iterators for anything that currently returns a slice you immediately iterate once and discard. That last one is the low-hanging fruit — you’re allocating memory you don’t need and Go’s GC is eating it, which is work you’re making the runtime do for no reason.
The O’Reilly Go resources are the fastest way to get current on the iterator model if you’ve been ignoring it since 1.23. The design documents are fine but they’re written for language designers; the practical guides are faster for “I just want to write the thing correctly.”
Sources
- Go 1.27 Release Notes — The Go Team
- Profile-guided optimization in Go 1.27 — The Go Blog