Go Programming

What's New in Go 1.27: Generic Methods, JSON v2 and uuid in the Standard Library

Go 1.27 finally adds generic methods to the language, promotes encoding/json/v2 into the standard library, ships a uuid package, and makes goroutine leak profiles generally available.

9 min read 1789 words
What's New in Go 1.27: Generic Methods, JSON v2 and uuid in the Standard Library

Most Go releases are a handful of library additions and a compiler that got a bit faster. Go 1.27, out in August 2026, is not that release. It changes the language — generic methods are finally here — and it lands two things people have been waiting years for: a second-generation JSON package and a uuid package, both in the standard library.

Generic Methods

This is the headline, and it is a real language change: a method declaration may now declare its own type parameters.

Before 1.27, type parameters could only appear on a function or on the type itself. If you wanted a generic helper that logically belonged to a type, your only option was a package-level function:

// Go 1.26 and earlier: the helper lives in the package namespace,
// even though it only makes sense for *Rand.
func N[Int intType](r *rand.Rand, n Int) Int

Now it can live where it belongs. math/rand/v2 is the standard library’s own example — it gained a generic method alongside the existing generic function:

// Go 1.27
func (r *Rand) N[Int intType](n Int) Int

Which means this now compiles:

r := rand.New(rand.NewPCG(1, 2))

var d time.Duration = r.N(time.Second)  // method, inferred as time.Duration
var i int32         = r.N(int32(100))   // same method, different instantiation

The practical effect is that you can stop writing pkg.DoThing(x, ...) for operations that are conceptually x.DoThing(...). Anywhere you have a container, a client, or a builder that needed a package-level generic helper, that helper can now be a method.

Two limits worth knowing before you plan a refactor around it:

  • Interfaces cannot declare type parameters on their methods, and an interface method cannot be satisfied by a generic method. So this does not give you generic interfaces through the back door; the method set of an interface is still fully concrete.
  • It is a method type parameter, distinct from the receiver’s. func (c *Cache[K, V]) MapTo[T any](...) has three type parameters in play, two from the receiver and one from the method.

If you have been writing the awkward package-level workarounds I described in how Golang generics minimize the amount of code you need to write, this is the release where a lot of that gets to move inside the type.

Two smaller language changes shipped alongside it. A key in a struct literal may now be any valid field selector, not just a top-level field name — which makes literals for structs with embedded types far less irritating. And function type inference is generalised to every context where a generic function is assigned to, or converted to, a matching function type, so a class of “cannot infer” errors simply disappears.

encoding/json/v2

The JSON rewrite is in, as two packages:

  • encoding/json/v2 — the high-level API you already know, redesigned.
  • encoding/json/jsontext — lower-level syntactic processing, with an Encoder and Decoder that work in terms of Token and Value and hold a state machine guaranteeing the JSON is valid.

The v2 entry points take variadic options:

import "encoding/json/v2"

b, err := json.Marshal(v,
    json.Deterministic(true),          // stable map ordering
    json.OmitZeroStructFields(true),
)

err = json.Unmarshal(data, &v,
    json.RejectUnknownMembers(true),   // strict decoding, no silent drops
)

Alongside Marshal and Unmarshal there are MarshalWrite / UnmarshalRead for an io.Writer / io.Reader, and MarshalEncode / UnmarshalDecode for driving a jsontext.Encoder / Decoder directly. That last pair is what you want for streaming, and it is a much better story than v1’s json.Decoder ever was.

The defaults are stricter, which is the whole point. v2 rejects invalid UTF-8 in strings and rejects duplicate names within an object — both of which v1 quietly accepted. That is more interoperable and it closes a genuine class of parser-differential bugs.

RejectUnknownMembers(true) deserves a callout for anyone writing an API. v1’s default of silently discarding unknown fields is how a typo in a client’s request body turns into a field that is quietly zero, and then into a support ticket. Making that an error at the boundary is the same instinct as the sentinel errors in error handling in Go: fail where the information is, not three layers down.

encoding/json v1 is not going anywhere, and it gained a matching set of options with names that tell you exactly what they are for — CallMethodsWithLegacySemantics, OmitEmptyWithLegacySemantics, FormatDurationAsNano, ParseTimeWithLooseRFC3339 and friends. That is the migration path: move to v2, and where behaviour differs in a way you depend on, opt back into the v1 semantics explicitly rather than discovering the difference in production.

One thing to be deliberate about: v2’s stricter parsing is a behaviour change at your HTTP boundary. A client that has been sending duplicate keys or malformed UTF-8 and getting away with it will start getting errors. That is correct, and it is still worth knowing before you flip it on a live service. Point a load test at it first — the setup in an easy way to load test your web apps is enough to see whether real traffic trips the new rules.

A uuid Package

There is now a uuid package in the standard library. The whole API is small enough to quote:

type UUID [16]byte

func New() UUID                        // the recommended default
func NewV4() UUID                      // random
func NewV7() UUID                      // time-ordered
func Parse(string) (UUID, error)
func MustParse(string) UUID
func Nil() UUID
func Max() UUID

func (UUID) String() string
func (UUID) Compare(UUID) int
func (UUID) MarshalText() ([]byte, error)
func (UUID) AppendText([]byte) ([]byte, error)
func (*UUID) UnmarshalText([]byte) error

NewV7 is the one to reach for when the UUID is a database key. Version 7 is time-ordered, so generated values sort roughly by creation time — which keeps B-tree index inserts near the right-hand edge instead of scattering them across the whole index the way v4 does. If you have ever watched write throughput degrade on a table with a random-UUID primary key, that is the problem v7 exists to solve.

Compare returning an int means it drops straight into slices.SortFunc, and the TextMarshaler / TextUnmarshaler implementation means it round-trips through JSON and database/sql without a wrapper type.

The obvious question: does this kill github.com/google/uuid? Not immediately — plenty of code depends on its wider surface. But for the common case of “I need a v4 or v7 UUID, and I need to parse one”, that is now a dependency you can delete.

Goroutine Leak Profiles, Generally Available

The goroutineleak profile, experimental in 1.26, is now GA in runtime/pprof and exposed at /debug/pprof/goroutineleak.

It reports goroutines blocked on a concurrency primitive that cannot possibly become unblocked. The detection is clever: it piggybacks on the garbage collector. If goroutine G is blocked on primitive P, and P is unreachable from any runnable goroutine — or from any goroutine those could unblock — then nothing can ever signal P, so G is leaked.

import _ "net/http/pprof"

// then:
//   go tool pprof http://localhost:6060/debug/pprof/goroutineleak

The reachability trick is also the limitation, and the release notes say so plainly: leaks caused by blocking on a primitive that is still reachable through a global, or through a runnable goroutine’s locals, will not be detected. It catches a large class, not all of them.

Still, this is the first tool that turns “we think we have a goroutine leak somewhere” into a list. The failure modes it finds are exactly the ones from worker pools in Go with errgroup — a worker blocked forever on a send to a channel nobody is reading — and the ones a missing <-ctx.Done() produces, which I wrote about in understanding Golang context.

Free Performance

The compiler now emits calls to size-specialised allocation routines, cutting the cost of small allocations (under 80 bytes) by up to 30%. The release notes are honest about what that means end to end: roughly 1% on allocation-heavy programs, and about 60 KB more binary, regardless of workload.

One percent for a recompile is a good trade. If it causes you trouble, GOEXPERIMENT=nosizespecializedmalloc turns it off at build time — but note that escape hatch is expected to be removed in Go 1.28, so treat it as a window to file a bug, not a setting to keep.

Smaller Things Worth Knowing

Change Why you care
strings.CutLast(s, sep) Cut from the right. The one you kept writing by hand with LastIndex.
testing/synctest.Sleep(d) Sleep inside a synthetic-time bubble, without the awkward dance around the fake clock.
hash/maphash.Hasher[T] and ComparableHasher[T] A real interface for hashing your own types — building a custom hash map stops meaning “reimplement hashing”.
net/http.Server.MaxHeaderValueCount (default 500) A cap on header value count, defending against header-flood requests.
net/http.Server.DisableClientPriority Ignore client-supplied HTTP/2 priority hints.
database/sql/driver.RowsColumnScanner Drivers can take over per-column scanning, which is how you avoid a round trip through interface{} for every value.
go doc pkg@version Read the docs for a specific version without changing your module.
go test runs stdversion by default Catches use of stdlib symbols newer than the Go version your go.mod declares.

That last one is quietly excellent. It is the failure mode where your code builds on your machine and breaks on a builder pinned to an older Go — the same class of problem I hit deploying this blog, where a template function that did not exist in the CI toolchain failed a build that was green locally.

Two for Later

crypto/mldsa implements ML-DSA, the post-quantum signature scheme standardised as FIPS 204, with the three parameter sets MLDSA44(), MLDSA65() and MLDSA87(). Unless you have a compliance requirement you will not touch it this year, but it being in the standard library is what makes migration a normal task rather than a project.

simd is an experimental package behind GOEXPERIMENT=simd, providing portable, vector-size-agnostic SIMD types like Int8s and Float32s that use hardware instructions where available. Experimental means experimental — but portable SIMD in Go is a genuinely interesting direction.

Should You Upgrade?

Yes, and the calculus is unusually simple:

  • The Go 1 compatibility promise holds; almost everything compiles unchanged.
  • You get ~1% on allocation-heavy code for a recompile.
  • goroutineleak is worth the upgrade on its own for any service running long-lived goroutines.
  • Generic methods and JSON v2 are opt-in. Nothing forces you to rewrite anything.

The only change that needs a moment’s thought is JSON v2’s stricter parsing, and only if you adopt it at a public boundary. Everything else is additive.

Conclusion

Generic methods close a gap that has been awkward since generics landed in 1.18, and they will quietly improve a lot of library APIs over the next year. JSON v2 is the rarer thing: a second attempt at a core package that gets to fix the defaults, with an explicit escape hatch back to the old behaviour. And a uuid package removes a dependency from nearly every service I have written.

Not bad for a release the notes describe as “mostly implementation”.