Back to Blog

What's New in Go 1.27: A Complete Guide

Go 1.27 ships generic methods, a stdlib uuid package, and json/v2 as the default engine. Here's what to adopt now, and what quietly breaks on upgrade.

What's New in Go 1.27: A Complete Guide

Go 1.27 adds five new standard library packages (encoding/json/v2, encoding/json/jsontext, crypto/mldsa, uuid, and simd) plus three language changes, and the upgrade is not entirely free. Some of it you'll want on day one, like generic methods and a uuid package that removes a dependency from most go.mod files. Some of it fails your test suite before you even notice the feature: compress/flate emits different bytes, function literals get different symbol names, and a handful of GODEBUG escape hatches now fail the build instead of restoring old behavior.

Table of Contents

Generic methods, and the one thing they still can't do

Since generics shipped in Go 1.18, type parameters worked in exactly two places: top-level functions and type declarations. Methods were excluded. If you wanted a transform that changed the element type of your collection, you had to write it as a package-level function, and package-level functions don't chain.

Here's the shape you've been writing. A metrics pipeline that turns sensor readings into display labels needs two transforms, and each one has to wrap the previous call:

example.gogo
type Metric struct {
    Sensor  string
    Celsius int
}

type List[T any] []T

// Package-level, because a method couldn't declare U.
func Map[T, U any](l List[T], f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    temps := Map(metrics, func(m Metric) int { return m.Celsius })
    labels := Map(temps, func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

In Go 1.27, Map becomes a real method that declares its own type parameter, independent of the receiver:

example.gogo
// U is the method's own type parameter, new in Go 1.27.
func (l List[T]) Map[U any](f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    labels := metrics.
        Map(func(m Metric) int { return m.Celsius }).
        Map(func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

Chaining is the visible win. The quieter one is naming: MapSlice, MapSet, and MapBox collapse into a single Map per type, because the receiver already says which type you mean. That receiver does work for the compiler too. List[Metric] pins T, so only the output type is left to infer. Methods are also discoverable in a way package-level helpers never are, since typing a dot lists everything the type can do and a helper function is something you have to already know about.

The standard library eats its own cooking here: math/rand/v2 now declares (*Rand) N[Int intType](Int) Int as a method alongside the package-level N function, which is exactly the shape the old restriction blocked.

Now the part the feature tours tend to skip. From the release notes:

So this does not compile:

example.gogo
type Transformer interface {
    // Compile error: interface methods can't declare type parameters.
    Map[U any](f func(Metric) U) any
}

The reason is structural. Interface dispatch happens at runtime, so the compiler can't know which instantiations of a generic method to generate for a dynamically dispatched call. If your API is built on an interface, transforms still need the old package-level function. That single restriction decides whether generic methods help your codebase or not, so check your interfaces before you plan a refactor. If you want the reps, the Go Generics Masterclass covers constraints and inference as in-browser exercises.

Verdict: adopt now, for concrete types. Leave interface-shaped APIs alone.

A uuid package in the standard library

Almost every Go service that talks to a database imports github.com/google/uuid. Go 1.27 makes that a standard library import path of just "uuid", following proposal #62026.

UUID is defined as [16]byte, which means values are comparable with == and usable as map keys directly. Three generators ship:

example.gogo
package main

import (
    "fmt"
    "uuid"
)

func main() {
    fmt.Println(uuid.New())   // reach for this when you don't care how it's made
    fmt.Println(uuid.NewV4()) // 128 bits, 122 of them random
    fmt.Println(uuid.NewV7()) // 48-bit timestamp first, so values sort by creation time

    requestID, err := uuid.Parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
    if err != nil {
        return
    }
    fmt.Println(requestID, requestID == uuid.Nil()) // false, it parsed fine
}

New is the default and currently returns a V4. NewV4 is fully random, so nobody can guess the next one, which is what you want in a public request ID. NewV7 puts a 48-bit timestamp up front, so newer values sort after older ones. That makes them good primary keys: your inserts land near the end of the index instead of scattering across it.

Watch the parentheses. Nil() and Max() are functions, not variables, so the comparison is id == uuid.Nil(). Nil() is the natural "not set yet" sentinel, and Max() is the all-ones value, handy as the upper bound when you scan a range of time-ordered V7 keys.

Verdict: adopt now for new code. Migrating an existing service is a find-and-replace, but see the FAQ before you delete the dependency.

Do you need to change anything for encoding/json/v2?

No, and that's the whole design. encoding/json is now backed by the v2 implementation, and the release notes are explicit that "Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ." You get the new engine's performance without touching a line. Per the release notes, "Marshal performance is broadly at parity with the previous implementation, while unmarshal performance is significantly faster."

You only opt into new behavior by importing encoding/json/v2 by name. That package picks stricter, more interoperable defaults: it rejects invalid UTF-8 in JSON strings, rejects duplicate names within a JSON object, matches field names case-sensitively, marshals a nil slice as [] rather than null, and marshals maps in a non-deterministic order where v1 guaranteed a deterministic one. That nil-slice change is the same nil-versus-empty distinction covered in var vs make in Go, and under v2 it finally shows up in your output.

example.gogo
import "encoding/json/v2"

type Event struct {
    ID     string `json:"id"`
    Action string `json:"action"`
}

// v1 would match the key "ID" against the tag `json:"id"`. v2 won't,
// and it skips the mismatched key silently rather than erroring.
var e Event
json.Unmarshal([]byte(`{"ID":"evt_01H","action":"checkout"}`), &e)
fmt.Println(e.ID, e.Action) // "" checkout

// v2 marshals maps in a non-deterministic order. Ask for a stable one
// explicitly when you hash or snapshot the output.
inventory := map[string]int{"widget": 12, "gadget": 3, "gizmo": 7}
b, _ := json.Marshal(inventory, json.Deterministic(true))

You don't switch imports to get v1 semantics back. You pass an option for the one behavior you need: Deterministic(true) for stable map order, MatchCaseInsensitiveNames(true) for loose field matching, FormatNilSliceAsNull(true) for null instead of []. The v1 package gained the same options, so you can adopt v2 semantics one behavior at a time without a full migration. The complete list lives under Migrating to v2 in the v1 package docs.

There's a third package too. encoding/json/jsontext handles the lower-level syntax, exposing JSON as a sequence of tokens and values with a state machine that keeps the output valid. It's the plumbing streaming codecs build on, and most code never touches it.

Here's how to pick:

flowchart TD
    A["Which JSON import?"] --> B{"Upgrading existing code?"}
    B -- Yes --> C["encoding/json<br/>v2 engine, v1 behavior"]
    B -- No --> D{"Want strict defaults<br/>and faster decoding?"}
    D -- No --> C
    D -- Yes --> E["encoding/json/v2"]
    E --> F{"Need raw tokens<br/>or streaming syntax?"}
    F -- Yes --> G["encoding/json/jsontext"]
    F -- No --> H["You're done"]

Decision tree for choosing between encoding/json, encoding/json/v2, and encoding/json/jsontext in Go 1.27.

Verdict: adopt the default now (you already have), adopt v2 by name later, once you've read the migration notes for your struct tags. The Data Formats course covers the tag rules that decide how much of this affects you.

Post-quantum signatures with crypto/mldsa

RSA and ECDSA are safe today because no computer can factor or solve discrete logs fast enough. A large enough quantum computer could. crypto/mldsa implements ML-DSA from FIPS 204 in three parameter sets, 44, 65, and 87, following proposal #77626.

It's wired into the rest of the stack already. crypto/x509 parses and verifies ML-DSA keys and signatures, and crypto/tls accepts all three parameter sets in a TLS 1.3 handshake. MLKEM1024 joins the supported key exchanges for quantum-safe key agreement, enabled by adding it to Config.CurvePreferences.

A full sign-and-verify for a firmware release:

example.gogo
sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
    return err
}

firmware, err := os.ReadFile("firmware-v2.1.0.bin")
if err != nil {
    return err
}

// Context is a domain-separation label. Sign and verify must pass the same one.
opts := &mldsa.Options{Context: "acme/firmware-release"}
sig, err := sk.Sign(nil, firmware, opts)
if err != nil {
    return err
}

fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // true
fmt.Println(mldsa.MLDSA65().SignatureSize())                         // 3309

firmware[0] ^= 1 // flip one bit
fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // false

Verify returns nil when the signature is good, so the comparison reads as a boolean. The cost is size, and it's not a rounding error:

Horizontal bar chart comparing digital signature sizes in bytes. Ed25519 is 64 bytes, ECDSA P-256 is about 71 bytes, RSA-2048 is 256 bytes, ML-DSA-44 is 2420 bytes, ML-DSA-65 is 3309 bytes, and ML-DSA-87 is 4627 bytes.

Source: signature sizes per FIPS 204 (ML-DSA) and the respective algorithm specifications.

An ML-DSA-65 signature is about 46 times the size of an Ed25519 one, and that size decides where it belongs. Firmware and release artifacts a device must still trust in ten years are worth 3 KB, and so are certificate roots meant to outlive today's crypto. Same goes for a TLS server you'd rather not re-key once quantum computers show up. For short-lived session tokens, where you mint thousands per second, it's real overhead you don't need yet.

One unrelated crypto/x509 change lands in the same area. SystemCertPool now respects SSL_CERT_FILE and SSL_CERT_DIR on Windows and Darwin, not just Linux. When those are set, Go loads roots from disk and uses its own verifier instead of the platform APIs. Set GODEBUG=x509sslcertoverrideplatform=0 to keep the old behavior.

Verdict: adopt for long-lived signatures, ignore for everything else.

The experimental simd package

A normal CPU instruction works on one value. A SIMD instruction works on a whole vector of them in the same step, so you get the same answer out of a fraction of the instructions. It pays off when you run identical math over a large slice of audio samples or image pixels, or when you take a dot product. For struct-and-string code it does nothing.

Every architecture exposes this differently. The new simd package is portable and vector-size-agnostic, available on all architectures, and it uses hardware instructions where they exist. Enable it with GOEXPERIMENT=simd at build time. It's experimental, so the API can change.

Mixing two audio tracks, several samples per step:

example.gogo
//go:build goexperiment.simd

// out[i] = trackA[i] + trackB[i], several samples at a time.
out := make([]float32, len(trackA))

lanes := simd.LoadFloat32s(trackA).Len() // how many float32s fit in one vector
for i := 0; i+lanes <= len(trackA); i += lanes {
    va := simd.LoadFloat32s(trackA[i:])
    vb := simd.LoadFloat32s(trackB[i:])
    va.Add(vb).Store(out[i:])
}
// A plain scalar loop handles the leftover tail.

Len reports how many elements fit in one vector on the machine you're running on, so the loop never has to hardcode a vector width. Each step loads that many samples from both tracks, adds them element-wise in one operation, and stores the result.

Verdict: ignore for now unless you already profile a numeric hot loop. The API is behind a GOEXPERIMENT and subject to change.

Performance you get just by upgrading

Rebuild and your program gets faster. The largest single item is size-specialized allocation, described in the release notes this way:

Chart showing the Go 1.27 size-specialized allocator. Allocations under 80 bytes get up to 30 percent cheaper, real allocation-heavy programs improve by about 1 percent overall, and binary size grows by a fixed 60 kilobytes.

Source: Go 1.27 release notes, faster memory allocation.

Read those two numbers together. The 30% applies to the allocation call itself for small objects, and the ~1% is what your service actually sees. If you hit a regression, GOEXPERIMENT=nosizespecializedmalloc opts out, and that escape hatch is expected to disappear in Go 1.28.

Three compiler optimizations also ship on by default. A known-bits dataflow pass tracks which bits of a value are provably 0 or 1 and folds away the resulting redundancy. Loop-invariant code motion lifts computations whose result never changes out of the loop body, so they run once instead of every iteration. And switch statements now compile to lookup tables where the cases allow it, including with fallthrough, jumping straight to the matching case instead of testing each one.

compress/flate gets faster too, with one caveat: the exact encoded output may differ from Go 1.26. DEFLATE sits underneath archive/zip, compress/gzip, compress/zlib, and image/png, so byte-exact golden tests can fail across all four. That's expected, not a bug. Re-bless the fixtures.

Verdict: adopt now. It arrives whether you want it or not.

Finding goroutine leaks and reading labeled tracebacks

The goroutineleak profile graduated from experiment to generally available, and the goroutineleakprofile GOEXPERIMENT is deleted. It's exposed through runtime/pprof and as the net/http/pprof endpoint /debug/pprof/goroutineleak.

A leaked goroutine is one blocked on a concurrency primitive that can never unblock. The runtime finds them through the garbage collector: if goroutine G is blocked on primitive P, and P is unreachable from any runnable goroutine or anything those could unblock, then G can never wake up.

example.gogo
func startJob() {
    result := make(chan int) // unbuffered: the send waits for a receiver
    go func() {
        result <- expensiveWork() // blocks forever, nobody receives
    }()
    // returns without receiving, so result becomes unreachable
}

func main() {
    startJob()
    runtime.GC() // the detector scans during a GC cycle
    pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
    // goroutineleak profile: total 1
    //   ... main.startJob.func1 ... main.go:14
}

The reachability trick has a blind spot worth knowing: the runtime can miss leaks where the blocking primitive is reachable through a global variable or through the locals of a runnable goroutine. A channel parked in a package-level registry stays "reachable," so the profile stays quiet. Concurrency Fundamentals walks through the leak patterns that produce these, and how to close them with context.

The other debugging win costs you nothing. For modules whose go directive is 1.27 or later, tracebacks now include runtime/pprof goroutine labels in the header line. The labels you already set for profiling show up in crash dumps, SIGQUIT traces, and runtime.Stack output. Often that label is the only thing separating two goroutines with identical stacks:

example.gogo
pprof.Do(ctx, pprof.Labels("request", id), func(ctx context.Context) {
    work()
})

// goroutine 34 [running]:
// labels: {"request":"req-42"}
// main.handle.func1 ...

Set GODEBUG=tracebacklabels=0 to turn it off, which matters if your labels carry anything you don't want in a crash dump.

Verdict: adopt now. Both are free, and the labels need no code change at all.

Smaller language and library wins

Struct-literal field selectors

A key in a struct literal may now be any valid field selector for the type, not just a top-level field name (proposal #9859). Embedding a shared model no longer forces you to nest it by hand:

example.gogo
type Model struct {
    ID        int64
    CreatedAt time.Time
}

type Post struct {
    Model
    Author string
    Likes  int
}

p := Post{Model: Model{ID: 42}, Author: "Patrik"} // before
p := Post{ID: 42, Author: "Patrik"}               // Go 1.27

Generalized function type inference

Function type inference now applies in every context where a generic function is assigned to, or converted to, a matching function type (proposal #77245). Previously only a typed variable declaration was enough:

example.gogo
func ascending[T cmp.Ordered](a, b T) int  { return cmp.Compare(a, b) }
func descending[T cmp.Ordered](a, b T) int { return cmp.Compare(b, a) }

orders := []func(int, int) int{ascending[int], descending[int]} // before
orders := []func(int, int) int{ascending, descending}           // Go 1.27

The slice's element type already says func(int, int) int, so Go reads T as int from that. The same now works for conversions and for passing a bare generic function as an argument.

strings.CutLast and bytes.CutLast

Cut splits on the first separator. CutLast splits on the last, returning the part before, the part after, and whether the separator was found (proposal #71151):

example.gogo
dir, file, found := strings.CutLast("internal/store/user.go", "/")
fmt.Println(dir, file, found) // internal/store user.go true

That replaces the LastIndex dance of finding the index, checking it for -1, then slicing twice by hand.

Rand.N, maphash.Hasher, and big.Int.Divide

math/rand/v2 gets Rand.N as a generic method (proposal #77853), so you can draw a bounded random value from a source you seeded rather than the unseedable global one. That makes a failing test replay with the same inputs.

hash/maphash gains a generic Hasher[T] interface, the contract between a type and future hash-based structures like custom maps and Bloom filters (proposal #70471). It bundles a hash function with an equality check, and the rule it enforces is that equal values must hash the same. ComparableHasher[T] is the ready-made implementation for comparable types that compares with ==. go/types already uses it, shipping a Hasher that respects the Identical relation.

math/big.Int gets a Divide method that computes quotient and remainder together under a rounding mode you choose: Trunc, Floor, Round, or Ceil. Quo and Mod always truncate toward zero, so this fills the gap for financial and numeric code.

Unicode 17, database/sql, and the rest

The unicode package and everything built on it moved from Unicode 15 straight to Unicode 17, so characters added in the two intervening releases now classify correctly. database/sql gained ConvertAssign, which gives drivers access to the same type conversions Rows.Scan performs instead of reimplementing the mapping.

The remaining entries are short enough to list:

  • crypto adds the MLDSAMu hash value, a signaling mechanism for external-mu ML-DSA signing.
  • crypto/ecdsa now checks that the hash length is correct in PrivateKey.Sign when you pass non-nil SignerOpts.
  • crypto/x509 exposes RawSignatureAlgorithm on Certificate, CertificateRequest, and RevocationList, giving you the DER-encoded AlgorithmIdentifier even when SignatureAlgorithm is UnknownSignatureAlgorithm. Parsing into pkix.Name also accepts a wider range of value types, with unknown ones landing in asn1.RawValue.
  • crypto/tls adds ConnectionState.LocalCertificate, the chain you presented to the peer, plus QUICConfig.ClientHelloInfoConn. Config.Rand is deprecated in favor of testing/cryptotest.SetGlobalRandom for deterministic tests.
  • net makes UnixConn read methods return io.EOF directly instead of wrapping it in a net.OpError.
  • runtime/secret propagates secret mode into goroutines created inside it.
  • go/constant adds StringLen, go/scanner adds Scanner.End, and go/token gives File a String method.
  • Ports: the big-endian 64-bit PowerPC port on Linux moves to the ELFv2 system ABI, which unlocks cgo, position-independent executables, and external linking there, and needs a Linux 3.13 kernel or later. On Plan 9, syscall.Errno is now defined and implements error, so portable code referring to it builds without build constraints. The linker also accepts -macos and -macsdk to set the versions written into the macOS LC_BUILD_VERSION load command.

What changed in the Go toolchain?

go fix gained four modernizers: embedlit simplifies references to embedded fields in composite literals, the cleanup the new struct-literal rule opens up. atomictypes replaces basic types in sync/atomic calls with atomic types, slicesbackward rewrites backward loops into slices.Backward, and unsafefuncs replaces unsafe pointer arithmetic with function calls. Two housekeeping changes came with them: fmtappendf was removed over stylistic concerns, and waitgroup was renamed to waitgroupgo. Run it once after you bump the go directive:

example.bashbash
go fix -diff ./...   # preview
go fix ./...         # apply

For the full catalog of what go fix rewrites, see every modernizer in go fix 1.26.

go test now runs the stdversion vet check by default. It reports standard library symbols that are newer than the Go version in force for the file, as set by your go.mod directive and build tags. If your module declares go 1.25 and someone reaches for strings.CutLast, you hear about it at test time instead of from a user on 1.25. go test -json also annotates "Action":"output" lines with an optional "OutputType" field, currently error, error-continue, or frame, which helps if you parse test output in CI.

go doc accepts package@version syntax now, so go doc rsc.io/[email protected] reads the docs for an exact release without checking it out. A new -ex flag lists a package's runnable examples, and naming one prints its source.

go mod tidy enforces a two-block layout for modules on go 1.27 or later, merging scattered require blocks into one direct and one indirect block. Comment blocks attached to dependencies are preserved, and a comment spanning a mixed set moves to the direct block.

go tool trace -http=:6060 now binds to localhost only, matching go tool pprof, so pass a full address like -http=0.0.0.0:6060 if you need it reachable from another machine. The compile, link, asm, cgo, cover, and pack tools also accept response files (@file) in a GCC-compatible format, which matters for build systems that blow past command-line length limits.

Testing and net/http

httptest.NewTestServer creates a Server on an in-memory fake network, built for use with testing/synctest, so there's no real TCP port involved. Its partner is synctest.Sleep, which does time.Sleep and synctest.Wait in one call, advancing the fake clock and then letting goroutines settle. Together they give you HTTP tests that are deterministic instead of timing-dependent. Professional Go Testing covers the synctest model these build on.

example.gogo
srv := httptest.NewTestServer(t, handler) // in-memory, cleanup auto-registered

On the net/http side, the HTTP/2 server now accepts client priority signals as defined in RFC 9218 and serves higher-priority streams first. Set Server.DisableClientPriority = true for the old round-robin behavior.

The change most likely to touch you is on HTTP/1: closing a partially-read Response.Body now drains the remaining content up to a conservative limit so the connection can be reused. For most programs that's a no-op or a small speedup. If you close early to abort a large download, Transport.DisableKeepAlives = true turns it off.

Server.MaxHeaderValueCount caps how many values a single header may carry, a cheap guard against header-flood requests. Transport and Server can negotiate TLS ALPN on a net.Conn you supply yourself, as long as it implements ConnectionState() tls.ConnectionState, so HTTP/2 gets picked up over tunneled or proxied connections. And net/url gained URL.Clone and Values.Clone for deep copies. If you build services on this, HTTP and Networking covers the server and transport knobs these attach to.

Verdict: adopt now. The httptest and synctest pair is the best reason to move a flaky test suite to 1.27.

What breaks when you upgrade to Go 1.27?

The release notes scatter the breaking changes across eight sections. Here they are in one place, ordered by how likely you are to hit them.

What changesWho it bitesWhat to do
compress/flate emits different bytesByte-exact golden tests over gzip, zlib, zip, or png outputRe-bless the fixtures. Compression is correct, just different.
Simpler function-literal (closure) namesTests asserting on symbol names, and code comparing function code pointers for equalityStop depending on literal names. Pointer comparison of funcs was already documented as unreliable.
asynctimerchan and gotypesalias GODEBUGs removed permanentlyAnything pinning them to the old value in go.mod or a //go:debug linegrep -r asynctimerchan . before upgrading. Setting the final default still builds, setting the old value fails.
Five TLS/x509 GODEBUGs removed: tlsunsafeekm, tlsrsakex, tls3des, tls10server, x509keypairleafServices still pinned to legacy TLS behaviorSame rule as above: the go command accepts the final default and rejects the old value, so you find these at build time.
macOS 13 Ventura is the minimumCI runners and dev machines on older macOSBump the runner image. Announced in the Go 1.26 notes.
bzr support removed from the go commandModules hosted on Bazaar serversMirror the dependency, or vendor it.
New //go:linknamestd directive marks std-only linknames, the linker now checks linkname access to assembly symbols, and type descriptors moved into a .go.type sectionPackages reaching into the runtime through an unsanctioned //go:linkname, yours or a dependency'sUpdate the dependency. These fail at build time, loudly. None of it is in the release notes, so a build is how you find out.
HTTP/1 Response.Body.Close drains unread contentCode that closes early to abort a large downloadSet Transport.DisableKeepAlives = true for those clients.
json/v2 error message text differsTests asserting on exact JSON error stringsMatch on error type or a substring instead of the full message.

The last row is the one that hides. The behavior is preserved but the text isn't, so a test suite that string-matches unmarshal errors will fail without telling you why. Isolate it with two runs on a branch that has already bumped go.mod to go 1.27:

example.bashbash
go test ./... > default.txt 2>&1
GOEXPERIMENT=nojsonv2 go test ./... > nojsonv2.txt 2>&1
diff default.txt nojsonv2.txt

Anything in the diff is JSON drift. Anything failing in both runs is something else in this table. GOEXPERIMENT=nojsonv2 is a temporary escape hatch and is expected to be removed in a future release, so treat it as a diagnostic, not a fix.

Frequently Asked Questions

Do I have to rewrite my JSON code for Go 1.27?

No. The encoding/json package is now implemented on top of v2, but marshaling and unmarshaling behavior is preserved and the v1 API stays supported. The only observable difference is that the exact text of error messages may change. You opt into the stricter v2 defaults only by importing encoding/json/v2 explicitly.

Can a generic method satisfy an interface?

No. The release notes state that interface methods may not declare type parameters, and that interface methods cannot be implemented by generic methods. Interface dispatch resolves at runtime, so the compiler can't know which instantiations to generate for a dynamically dispatched call. If your API is built on interfaces, keep using package-level generic functions.

Should I drop github.com/google/uuid for the standard library package?

For new code, yes. For existing code, check two things first. The standard library UUID is [16]byte with its own method set, so any code touching the third-party type's extra methods needs review, and any dependency that exports the third-party uuid.UUID in its API keeps that module in your graph regardless.

Is crypto/mldsa ready for production?

It's a stable standard library package implementing FIPS 204, wired into crypto/x509 and crypto/tls. The practical constraint is signature size, not maturity: ML-DSA-65 signatures are 3309 bytes against 64 for Ed25519. Use it where the signature must outlive today's cryptography, and skip it for high-volume short-lived tokens.

What is the fastest way to find what Go 1.27 breaks in my codebase?

Bump go.mod to go 1.27 on a branch, then grep for asynctimerchan and gotypesalias and the five removed TLS settings, since those fail the build outright. Then run your suite twice, once normally and once with GOEXPERIMENT=nojsonv2, and diff the failures. That separates JSON error-text drift from golden-file and symbol-name failures.

Sources

Primary references cited in this post (last verified August 23, 2026):

Keep going

Generic methods only pay off if constraints and inference are second nature, and that's the part that's easy to skip when you first meet generics. The Go Generics Masterclass works through constraints, type sets, and inference as in-browser exercises that run on the current toolchain, so you can try the 1.27 method shape against real code instead of reading about it.

Newer to Go? Start with the Go Fundamentals track and come back for the release notes. And if you skipped last year's release, What's New in Go 1.26 covers errors.AsType, the Green Tea garbage collector, and new(expr).

Ready to master Go?

Join LevelUpGo and start building real projects with interactive, hands-on lessons.

Start Learning Free