Go 1.27 closes a five-year gap in the type system, swaps the JSON engine out from under every program that marshals anything, and quietly puts post-quantum signatures in the standard library. Here is what changed, what it costs you, and what will break.

I put this together as a talk for Belfast Gophers, and the framing I kept coming back to is the one in the title. Go is seventeen years old. Every line of Go you wrote in 2015 still compiles. gofmt still gives exactly one answer. That is a genuinely rare achievement in this industry, and it is the thing that makes a release like 1.27 interesting rather than alarming — because 1.27 contains a complete rewrite of the most-used package in the standard library, and the migration path is do nothing.

This is the long version of that talk, with the detail I had to cut for time, plus a section on post-quantum cryptography that did not make the deck at all.

Where Go actually is in 2026

It took a while to reach 1.0, but the trajectory since has been unusually steady. Around 2.2 million developers now list Go as a primary language — twice as many as five years ago, and over five million once you count those for whom it is a secondary language. Roughly 11% of developers say they plan to adopt it in the next twelve months, and it sits fourth in JetBrains' Language Promise Index, behind only TypeScript, Rust and Python.

What is more telling is where that growth shows up. Two archetypes dominate the survey data: web backends — services, microservices, APIs — and DevOps, SRE and platform work, meaning Kubernetes operators, CLI tooling, infrastructure-as-code. Go's growth appears in infrastructure job specs, not keynote hype cycles. And, as we will get to, a third archetype is now forming on top of the second.

The reasons it earns its place have not changed much:

The standard library is a security posture, not an aesthetic

Illustration of the Go gopher dressed as an archaeologist, opening a treasure chest by torchlight

2026 has been a rough year for package ecosystems, and it is worth being specific rather than smug about it. Sonatype catalogued over 454,600 new malicious open-source packages in 2025, pushing the cumulative total past 1.233 million — and, in their framing, marking the shift from spam and stunts to sustained, industrialised campaigns against the people and tooling that build software.

Two incidents stand out. On 31 March, axios — roughly 100 million downloads a week, 174,000 dependent packages — shipped a cross-platform RAT for just under three hours after the lead maintainer's npm credentials were compromised. Two backdoored releases went out, one of them tagged latest. CISA issued an alert, and Microsoft Threat Intelligence attributed the infrastructure to Sapphire Sleet, a North Korean state actor. Then on 4 August, the keyv / cacheable compromise: another hijacked maintainer account, but this time carrying a self-propagating worm — CHAINDROP, a descendant of the Shai-Hulud family — that used stolen npm credentials to automatically backdoor every other package the maintainer could publish to. Elastic Security Labs counted 444 packages across 1,381 versions, with over two billion monthly installs between them. The poisoned releases carried valid provenance, signed by GitHub Actions.

That word — worm — is the qualitative shift. Not isolated bad packages, but self-replicating campaigns that steal credentials in order to publish more compromised packages, propagating through trust infrastructure you cannot opt out of.

The interesting question is not “why is npm bad.” It is: why does a compromised package get to run code on your laptop at all? The answer is lifecycle scripts. npm install executes preinstall and postinstall hooks from packages you have never read. Datadog's write-up of the keyv worm traces exactly that chain: every affected package gained a setup.mjs file and a "preinstall": "node setup.mjs" entry in its package.json, which ran on npm install without anything ever importing the package.

The actual argument

go get and go build do not execute arbitrary code from your dependencies at install time. There is no hook. Malicious Go code has to actually be called by your program to do anything. That is not a policy, or a linter, or a scanner you can forget to run — it is an absence in the design.

Around that absence Go stacks a few things that matter: immutable module versions, so a published version's bytes never change; a checksum database at sum.golang.org acting as a tamper-evident transparency log; a module proxy, so builds are not hitting arbitrary Git hosts; and govulncheck, which is first-party and reports only the vulnerabilities you actually reach.

That last distinction is the underrated one. Most scanners tell you “you depend on something with a CVE.” govulncheck tells you “you call the affected function.” That is the difference between a security tool people use and one they mute in CI.

And the honest counter-example

Go is not immune, and the most instructive case is one where the defence became the weakness. boltdb-go/bolt was a typosquat of boltdb/bolt, backdoored and published in November 2021 — and because module immutability is exactly what makes builds reproducible, the malicious version stayed cached and available in the module mirror for roughly three years, undetected until February 2025. The attacker had rewritten the Git tags to point at benign code, so the repository looked clean while the mirror kept serving the backdoor.

There are others: shopsprint/decimal against shopspring/decimal — one letter, a g swapped for a t — which sat benign for almost six years, mirroring the real library's releases, before a 2023 version added an init() that opened a DNS TXT command channel. And fake MongoDB Go drivers with persistent backdoors, caught by GitLab in 2025. The trade-off is real and worth stating plainly rather than pretending it away.

The framing I would offer is this: dependency count is a proxy for a better question — how many maintainer accounts' 2FA am I trusting tonight? Every module in your graph is a person whose account is part of your threat model. The stdlib-first instinct is not purity. It is that every dependency you do not add is an account you do not have to trust.

The AI infrastructure layer is being written in Go

Illustration of the Go gopher at a workstation surrounded by LLM, RAG and vector database references

To be fair to Python first: for exploration, training and research it remains unmatched, and nobody sensible is writing a training loop in Go. The claim here is narrower and it is about the layer around the model.

Training / researchProduction / serving
PythonGo
PyTorch, JAX, NumPy, pandasServing, orchestration, agent runtimes
Notebooks, iterationSingle binaries, deployment

The strongest single data point: Ollama, the most popular local LLM runtime in the world, is a Go program. So are Kubernetes, Docker and containerd. MCP has an official Go SDK.

The reason is the same argument as the section above. When you ship an agent or an inference server, you are shipping to someone else's machine. A single static binary means no interpreter, no virtualenv, and no CUDA-version-in-a-requirements-file archaeology. The Kubernetes and SRE crowd did not migrate to AI work — AI work moved onto their substrate.

Language changes

Struct literal field selectors

Pure ergonomics. A struct literal key can now be any valid field selector, so promoted fields from embedded structs work directly.

type Base struct{ ID int }
type User struct {
    Base
    Name string
}

// Before 1.27:
u := User{Base: Base{ID: 7}, Name: "Mittens"}

// Go 1.27:
u := User{ID: 7, Name: "Mittens"}

This was issue #9859. A four-digit issue number, filed in 2015. It is a small thing, but it is also a fair illustration of the pace: Go would rather take eleven years than take it back.

Generalised function type inference

Inference now applies in all contexts where a generic function is assigned or converted to a matching function type — not just plain assignment, but conversions and composite literals too (issue #77245). Most generics work since 1.18 has been the type checker gradually learning what programmers already assumed it knew. This is another instalment.

Generic methods — the headline

The fix only lands if you have felt the constraint. Since 1.18, only top-level functions could be generic. A method could use its receiver's type parameters, but it could not introduce its own. So any generic operation that changes the element type — map, fold, transform — had to be a package-level function:

func Map[T, U any](b Box[T], f func(T) U) Box[U]

Not discoverable by autocomplete on the type, not chainable, and it pollutes the package namespace. Every Go generics library since 1.18 hit this wall. If you have used samber/lo and wondered why everything is a free function taking the collection first, this is why.

In 1.27, methods may declare their own type parameters:

type Box[T any] struct{ v T }

// The method declares its OWN type parameter U — new in 1.27
func (b Box[T]) Map[U any](f func(T) U) Box[U] {
    return Box[U]{v: f(b.v)}
}

b := Box[int]{v: 21}
doubled := b.Map(func(n int) int { return n * 2 })
label := doubled.Map(func(n int) string {
    return fmt.Sprintf("value=%d", n)
})
fmt.Println(label.v) // value=42

Follow the types through that chain: Box[int]Box[int]Box[string]. It was not expressible last month. The standard library already uses it — math/rand/v2 gains (*Rand).N[Int intType](Int) Int, a method matching the top-level N function.

The proposal is #77273, by Robert Griesemer.

The limit: interfaces stay monomorphic

This is the part people misremember, so it is worth being exact. Interface methods cannot declare type parameters. Not “not yet” in the usual sense — this is a considered boundary that falls out of how generics are implemented.

Diagram comparing a concrete call, where the compiler knows T=int and U=string and can build a dictionary, with an interface call where the concrete type is unknown and the dictionary has no source
At a concrete call site the compiler can build the dictionary. At an interface call site there is nowhere for it to come from.

Go implements generics partly by dictionary passing: the compiler hands instantiated functions a hidden table of type information — sizes, method sets, how to copy and compare. At a concrete call site the compiler knows every type argument, so it can construct that table at compile time.

At an interface call site it does not know the concrete type. That is the entire point of an interface. So there is nowhere for the dictionary to come from. The alternatives are to monomorphise every possible instantiation — binary size explosion, and impossible across package boundaries, because you cannot know what types downstream code will use — or to reify types at runtime, which is a different language with a different performance model.

So: generic methods are for concrete types. If someone asks whether this will ever change, the honest answer is that it would need a different implementation strategy for generics wholesale. Do not hold your breath.

The objection worth taking seriously

There is a well-circulated critique — ThePrimeagen has a video on it, and there is a good deal of quieter grumbling — that runs roughly like this. Go's value was never its feature list; it was constraint. One formatter, one obvious way, every codebase legible on day one. Generics, iterators and now generic methods each add a way to express something, and cumulatively they turn a language with one dialect into a language with several.

The canary is Result[T]. Now that Then, Map and FlatMap are expressible as methods, some proportion of the ecosystem will spend the next year writing monadic result types. You will open a codebase and find a parallel error idiom that does not talk to errors.Is. And the counter-argument the Go team used for years to reject fancier error handling — that it would fragment the ecosystem — applies here with equal force.

One concession is simply correct: Go has no variadic type parameters. You cannot write one generic Convert that handles any arity; you write Convert0, Convert1, Convert2. TypeScript's variadic tuple types handle this and Go's do not. Import a pattern from a richer type system and you get the pattern and its boilerplate.

Where I land

The legitimate concern is not the Go team's judgement — it is ecosystem drift: a popular library adopting Result[T] and dragging its dependents into a second error idiom. That hazard is not gated by anything in the compiler. It is gated by taste and code review. When you see Result[T] in a PR, the question is not “is this clever” but “does this interoperate with errors.Is, errors.As and %w, and can the next person read it cold?” Usually the answer is no, and that is your review comment.

Generic methods are a real gap closed for container types — sets, trees, iterators, typed caches. Frame it that way and you will be right in eighteen months.

JSON v2: the change you did not choose

Illustration of the Go gopher riding a rocket

This is the change in 1.27 that touches the most code, and it is the one you did not opt into. It is also far less alarming than that sounds — and the gap between those two facts is the most interesting thing in the release.

encoding/json/v2 and encoding/json/jsontext are now available without GOEXPERIMENT=jsonv2. More importantly, the classic encoding/json is now backed by the v2 implementation underneath. Every Go program that marshals JSON — so, every Go program — gets the new engine on upgrade.

What you get for free: unmarshal is significantly faster, marshal is broadly at parity, no migration is required, no API changes, and v1 continues to be supported. The compatibility discipline here is genuinely impressive — they rewrote the most-used package in the standard library and the migration path is “do nothing.”

If you need out, the opt-out is GOEXPERIMENT=nojsonv2 at build time, and it is expected to be removed in a future release. Treat it as a stopgap, not a strategy.

What actually changes for v1 callers

Less than the rumour mill suggests, and it is worth being precise, because this is where most of the upgrade anxiety is pointed. The v1 package is now implemented in terms of v2 with a set of options that pin the historical behaviour — Marshal and Unmarshal call their v2 counterparts with DefaultOptionsV1(). Marshaling and unmarshaling behaviour is preserved.

That includes the thing people most expect to break. Map key ordering is unchanged for v1 callers. encoding/json still documents that map keys are sorted, and they still are. If you stay on v1, your golden files full of marshalled maps will keep passing.

The one documented v1-visible difference is error message text. If you string-match on error strings — you should not, but people do — that is where it will bite.

And if you hit something genuinely incompatible, GOEXPERIMENT=nojsonv2 restores the original v1 implementation at build time. It is expected to be removed in a future release, so treat it as a stopgap rather than a strategy.

Where the sharp edges actually are

They are in migrating to the v2 API, which is a deliberate act rather than something an upgrade does to you. v2 changes a number of defaults on purpose — the package docs enumerate fifteen-odd behavioural differences — and three are worth knowing before you start:

The first is the one that will find your test suite. This is the shape of it:

// config.go
type Config struct {
    Name   string         `json:"name"`
    Limits map[string]int `json:"limits"`
}

// config_test.go — passes on v1; fails the moment this call moves to v2
func TestGolden(t *testing.T) {
    c := Config{
        Name: "prod",
        Limits: map[string]int{
            "zulu": 1, "alpha": 2, "mike": 3, "bravo": 4,
        },
    }
    got, _ := json.Marshal(c)
    want, _ := os.ReadFile("testdata/config.golden.json")
    if !bytes.Equal(got, bytes.TrimSpace(want)) {
        t.Errorf("golden mismatch:\n got: %s\nwant: %s", got, want)
    }
}

That is a forty-line package. Now imagine a service with two hundred fixture files full of API responses and a migration PR that moves them all at once. The failure output does not help you either — you get a byte diff on JSON that looks identical apart from ordering.

Action item

Upgrading to 1.27 does not require you to do anything about JSON. Migrating to encoding/json/v2 does — so do it deliberately, package by package, reaching for json.Deterministic(true) wherever you depend on stable output. The second-order point is the more useful one: if reordering bytes can break your tests, you are asserting on an encoder's incidental choices rather than on your program's behaviour. Compare parsed structures, not bytes.

The new API surface

encoding/json/v2 gives you Marshal and Unmarshal as you would expect, plus MarshalWrite, MarshalEncode, UnmarshalRead and UnmarshalDecode, all taking variadic Options. encoding/json/jsontext covers the syntactic layer: Encoder and Decoder over Token and Value.

The API mirrors v1 for the common case, which is rather the point. The interesting part is the options mechanism: v2 made behaviour configurable rather than baked in, which is how a rewrite of this size got past the compatibility promise at all. Credit to Joe Tsai and Damien Neil, proposal #71497, in flight since 1.25.

Post-quantum cryptography lands in the standard library

Illustration of the Go gopher in a security uniform holding a shield with a padlock

This did not make my talk and it should have, because it completes a story Go has been telling since 1.24.

The threat model is “harvest now, decrypt later”: an adversary records encrypted traffic today and decrypts it once a cryptographically relevant quantum computer exists. That makes the migration urgent for confidentiality long before the machine exists, because the traffic being captured is today's traffic. Signatures are less exposed to harvesting — a signature verified today cannot be retroactively forged — but certificate chains and code-signing roots have long lifetimes, so they need lead time too.

Go has been addressing the first half for two releases. crypto/mlkem landed the ML-KEM key-encapsulation mechanism in Go 1.24, and the hybrid X25519MLKEM768 key exchange became a TLS default. 1.27 does two things: it finishes the key-exchange story, and it adds signatures.

crypto/mldsa — post-quantum signatures

The new crypto/mldsa package implements ML-DSA, the post-quantum digital signature scheme specified in FIPS 204. It ships three parameter sets — MLDSA44, MLDSA65 and MLDSA87 — trading key and signature size for security level.

priv, _ := mldsa.GenerateKey(mldsa.MLDSA65())

msg := []byte("ship it")
sig, _ := priv.Sign(rand.Reader, msg, crypto.Hash(0))

fmt.Println("scheme:  ", mldsa.MLDSA65())
fmt.Println("sig size:", mldsa.MLDSA65().SignatureSize())
fmt.Println("verified:", mldsa.Verify(priv.PublicKey(), msg, sig, nil) == nil)

One detail worth knowing before you build on it: Verify takes an *Options, and nil is equivalent to the zero value. The Options.Context field — at most 255 bytes, empty by default — domain-separates signatures created for different purposes, and the same context must be used to sign and to verify. It is a cheap way to stop a signature minted for one purpose being replayed as valid for another.

Be aware of the size story before you reach for it. This is the cost of lattice-based security, and it lands on your certificate sizes, your handshake bytes, and anything that embeds a signature in a header or a token:

SchemeSignaturePublic keySeed
Ed25519 (for comparison)64 B32 B32 B
MLDSA442,420 B1,312 B32 B
MLDSA653,309 B1,952 B32 B
MLDSA874,627 B2,592 B32 B

An ML-DSA-65 signature is roughly fifty times the size of an Ed25519 one. On a certificate chain that is bytes on every single handshake; on a signed token it can be the difference between fitting in a header and not. Measure before you swap.

Integration across x509 and TLS

The package would be an academic curiosity on its own. What makes it usable is that it is wired into the rest of the crypto stack:

Key exchange: MLKEM1024 joins the party

crypto/tls now supports the MLKEM1024 key exchange, enabled through Config.CurvePreferences:

cfg := &tls.Config{
    CurvePreferences: []tls.CurveID{
        tls.X25519MLKEM768, // hybrid, the sensible default since 1.24
        tls.MLKEM1024,      // new in 1.27
    },
}

There is a useful wrinkle in the policy layer. Post-quantum hybrid key exchanges can now be explicitly enabled in Config.CurvePreferences even if the tlsmlkem=0 or tlssecpmlkem=0 GODEBUG options are set. In other words, an organisation-wide GODEBUG kill switch no longer silently overrides a service that has deliberately opted in. Explicit configuration wins over ambient policy, which is the right precedence.

What to actually do

For transport confidentiality, you are probably already covered — X25519MLKEM768 has been on by default since 1.24, and if you have not overridden CurvePreferences, your TLS 1.3 handshakes are hybrid already. For signatures, this is a “start testing” release rather than a “migrate now” one: your certificate authority, your peers and your hardware all have to agree before ML-DSA certificates are useful in public PKI. Where it is immediately usable is internal PKI you control end to end, and long-lived signing where the artefact outlives the algorithm — firmware, release artefacts, document signing.

The rest of the crypto changes are smaller but worth noting: crypto/ecdsa's PrivateKey.Sign now validates hash length when given non-nil SignerOpts; crypto/x509 gains RawSignatureAlgorithm fields on Certificate, CertificateRequest and RevocationList; SystemCertPool now respects SSL_CERT_FILE and SSL_CERT_DIR on Windows and macOS, which quietly fixes a long-standing container-vs-desktop inconsistency. And tls.Config.Rand is deprecated in favour of testing/cryptotest.SetGlobalRandom.

The goroutine leak profiler graduates

Illustration of the Go gopher as a plumber tightening a leaking pipe with a wrench

In 1.26 this sat behind GOEXPERIMENT=goroutineleakprofile. In 1.27 it graduates and the experiment flag is deleted. It is available through runtime/pprof and — more usefully — as /debug/pprof/goroutineleak in net/http/pprof, which is how you would actually use it: scraping a running service rather than writing to stdout.

func leak() {
    ch := make(chan int) // only this goroutine ever sees ch
    ch <- 1              // blocks forever: nobody will ever receive
}

func main() {
    go leak()
    runtime.Gosched() // let it park on the send

    pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
}

The mechanism is elegant: the detector uses the garbage collector. If goroutine G is blocked on primitive P, and P is unreachable from any runnable goroutine — or any goroutine those could unblock — then P can never be signalled, so G can never wake. That is a leak, by construction.

The limitation deserves stating plainly, because a tool whose failure modes you understand is more useful than one you trust blindly. Because it is reachability-based, it will not find leaks where the blocking primitive is reachable through a global variable, or through the locals of a runnable goroutine.

The design document is by Georgian-Vlad Saioc and Milind Chabbi, both at Uber — the same group behind goleak and LeakProf, which is to say this arrived in the standard library having already been run against a very large production Go estate.

Pair this with the other debugging change: tracebacks now include runtime/pprof goroutine labels in the header line for modules declaring go 1.27 or later. If you already use pprof.Do with labels, that context now shows up in crash dumps and SIGQUIT traces — goroutine 1 [running] {request: 42}:. Disable with tracebacklabels=0. Between the two, 1.27 is quietly a strong release for debugging production.

How Go ships risk: GOEXPERIMENT

Illustration of the Go gopher as a scientist in a laboratory holding a smoking flask

This is the intellectual payoff of the whole release. Go has a mechanism most languages lack. Features land in the tree behind a build-time flag, fully implemented but explicitly outside the compatibility promise. Then one of three things happens:

1.27 shows two of the three. goroutineleakprofile graduated outright. jsonv2 graduated and inverted — it is now the default, with nojsonv2 as the escape hatch. This is how Go ships ambitious things without breaking the Go 1 promise, and it is an underrated piece of project design.

runtime/secret, and why it is not a security guarantee

runtime/secret is the counter-example: still experimental, still behind GOEXPERIMENT=runtimesecret. It exists for forward secrecy — erasing key material that would otherwise linger in memory.

secret.Do(func() {
    // ephemeral key material lives here;
    // registers and stack are zeroed on return
})

Do invokes f and ensures temporary storage used by f is erased in a timely manner: registers and stack are erased before Do returns, and heap allocations are erased once the garbage collector realises the values are unreachable. In 1.27, goroutines created while executing in secret mode now themselves execute in secret mode — which closes an obvious hole in the original design.

The caveats are the important part. It is only supported on linux/amd64 and linux/arm64; on other platforms Do simply invokes f directly. And because it requires a build-time GOEXPERIMENT, it cannot be a security guarantee in a binary that somebody else compiles. Use it in your own release pipeline, where you control the build. Do not put it in a library and call it a defence.

The standard library keeps absorbing your dependencies

Illustration of the Go gopher as a refuse collector wheeling a bin towards a rubbish truck

The headline here is uuid — a standard library UUID package at last. RFC 9562, crypto/rand-backed, with New(), NewV4() and NewV7(). V7 is time-ordered, which makes it a much better database key than V4. google/uuid is in an enormous number of go.mod files and is about to leave a lot of them. That is one fewer dependency, on exactly the argument made further up this page.

The rest of the haul:

Performance and internals

Faster allocation. Size-specialised allocation routines cut the cost of small allocations — under 80 bytes — by roughly 30%, for about 60 KB of binary size. Disable with GOEXPERIMENT=nosizespecializedmalloc, which is expected to disappear in 1.28.

HTTP/2 is finally a real package. h2_bundle.go — a mechanically generated 12,226-line file — is gone, replaced by net/http/internal/http2. HTTP/3 scaffolding is quietly in the tree as unexported pluggable hooks; there is nothing to call yet.

Three new compiler optimisations, on by default: known-bits dataflow, loop-invariant code motion, and switch-to-lookup-table conversion.

Unsanctioned //go:linkname gets harder, with a new linknamestd directive and linker checks on linkname access to assembly symbols. If you depend on a linkname Go never blessed, test against 1.27 early.

os.Root closed another escapeReadDir/Readdir could previously escape a root.

And on the experimental bench, the new simd package (behind GOEXPERIMENT=simd) offers portable, vector-size-agnostic SIMD across all architectures, while simd/archsimd adds arm64 Neon and WebAssembly 128-bit support alongside a revised amd64 API. The API is intentionally non-portable and explicitly not stable. Revisit in 1.29.

What will break: the upgrade checklist

The verdict

ChangeWhat to do
Goroutine leak profileAdopt now. Zero cost until you scrape it. Wire the endpoint into every service this week.
JSON v2Upgrade freely, migrate deliberately. v1 semantics are pinned, so the toolchain bump is safe. Save the afternoon for whenever you actually move a package to the v2 API.
Post-quantum (ML-DSA)Start testing. Internal PKI and long-lived signing first. Public PKI needs your CA and peers to catch up.
Post-quantum (ML-KEM)Already done, probably. Verify you have not overridden CurvePreferences and lost the hybrid default.
uuidAdopt on next touch. Not worth a dedicated PR; delete google/uuid when you are next in that file.
Generic methodsYes for containers, no for monads. Sets, trees, iterators, typed caches — go. Result[T] — wait a year.
httptest.NewTestServerAdopt in new tests. Especially if you already use synctest.
runtime/secretYour own pipeline only. Cannot be a guarantee in a binary someone else compiles.
simdIgnore. Unstable API, experiment flag. Revisit in 1.29.
math/big.Int.DivideAdopt if you touch money. Quo/Mod's rounding has been quietly wrong in financial code for years.

The bottom line

Three things to remember from this release.

Generic methods close a five-year gap in the type system — and the guardrail against misusing them is you, not the compiler.

JSON v2 is under your v1 code whether you asked for it or not — and the remarkable part is how little that costs you. The sharp edges live in migrating to the v2 API, which is a choice you make rather than one the upgrade makes for you.

The standard library keeps absorbing your dependencies — and in 2026 that is a security posture, not an aesthetic. Post-quantum signatures landing in crypto/mldsa rather than in three competing third-party modules is the same argument, applied to the part of your stack where getting it wrong is least recoverable.

Go has changed less than the discourse suggests and more than the purists would like. Every line of Go you wrote in 2015 still compiles, gofmt still gives exactly one answer, and the standard library's taste has not moved. What has changed is that you now have more rope — and the community, rather than the compiler, decides how much of it you use.

The full release notes are at go.dev/doc/go1.27.