Go 1.27 is coming soon, so it’s a good time to get a head start on what’s new. The official release notes are pretty dry, so here’s a hands-on version with runnable examples showing what changed and how the new behavior works.
A quick credit first: the interactive Go tours were started by Anton Zhiyanov, who wrote one for every release from Go 1.22 through Go 1.26. He’s decided to stop, so we’re picking up where he left off. His earlier tours are all still worth a read:
Go 1.22 interactive tour
Go 1.23 interactive tour
Go 1.24 interactive tour
Go 1.25 interactive tour
Go 1.26 interactive tour
Thanks, Anton.
Before we start digging into the new features, let’s set the context.
This article is based on the official release notes and the Go source code, licensed under the BSD-3-Clause. This is not an exhaustive list; see the official release notes for that.
Links point to the documentation (𝗗), proposals (𝗣), most relevant commits (𝗖𝗟), and authors (𝗔) for each feature; check them out for motivation, usage, and implementation details. The authors (𝗔) are the people who contributed to the feature (writing the implementation, the tests, or, for features that graduated from an earlier experiment, the original design), not necessarily a single main author.
Error handling is often skipped to keep the examples short. Don’t do this in production ツ
Generic methods
#
This is the headline of the release. A method declaration may now declare its own type parameters, independent of the receiver’s. Before Go 1.27, only top-level functions could be generic, so a generic operation on a type had to live as a package-level function instead of a method.
Say we have a generic container and want a Map operation that can change the element type:
type Box[T any] struct{ v T }
// The method declares its own type parameter U (new in Go 1.27). func (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{v: f(b.v)} }
Now Map is a method of Box and can transform an int box into a string box:
func main() { 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
There is one important restriction: interfaces still can’t declare type-parameterized methods, and a generic method can’t be used to satisfy an interface. Put a generic method in an interface and the compiler stops you:
type Mapper interface { Map[U any](f func(int) U) any // interfaces can’t declare generic methods }
interface method must have no type parameters
𝗗 Generic methods
𝗣 77273
𝗖𝗟 524b860, e84da04, e212a16
𝗔 Robert Griesemer, Mark Freeman
Struct literal field selectors
#
A key in a struct literal may now be any valid field selector for the struct type, not just a top-level field name. In practice this means you can set a promoted field (one that comes from an embedded struct) directly, without spelling out the embedded type.
type Base struct { ID int }
type User struct { Base Name string }
Before Go 1.27 you had to write User{Base: Base{ID: 7}, Name: “Mittens”}. Now the promoted ID works as a key on its own:
u := User{ID: 7, Name: “Mittens”} fmt.Println(u.ID, u.Name)
7 Mittens
𝗗 Composite literals
𝗣 9859
𝗖𝗟 1a8f9d8, 9f7e98d, 30bfe53, e2c1885
𝗔 Robert Griesemer, Cherry Mui
Generalized function type inference
#
Function type inference has been generalized to apply in all contexts where a generic function is used where a matching function type is expected: not just plain assignment to a variable (which already worked), but also conversions and composite literals. In those cases you previously had to spell out the type arguments by hand.
Take two generic helpers and drop them into a slice whose element type is func([]int) int:
func first[T any](s []T) T { return s[0] } func last[T any](s []T) T { return s[len(s)-1] }
// The slice’s element type drives inference: T=int for each entry. // Before Go 1.27 this failed with “cannot use generic function // without instantiation”; you had to write first[int], last[int]. ops := []func([]int) int{first, last} for _, op := range ops { fmt.Println(op([]int{10, 20, 30})) }
10 30
𝗗 Assignability
𝗣 77245
𝗖𝗟 ef06728, f757de8
𝗔 Robert Griesemer, Mark Freeman
Faster memory allocation
#
The compiler now generates calls to size-specialized memory allocation routines, cutting the cost of some small (under 80 bytes) allocations by up to 30%. Improvements vary with the workload, but the overall gain is expected to be around 1% in real allocation-heavy programs. The tradeoff is about 60 KB of extra binary size, independent of the workload.
There’s nothing to change in your code; it just gets a little faster. If you need to turn it off, build with GOEXPERIMENT=nosizespecializedmalloc. That opt-out is expected to be removed in Go 1.28.
𝗗 Runtime release notes
𝗣 79286
𝗖𝗟 2a93576
𝗔 Michael Matloob
Goroutine labels in tracebacks
#
For modules whose go.mod sets Go 1.27 or later, tracebacks now include runtime/pprof goroutine labels in the header line of each goroutine. If you already attach labels for profiling with pprof.Do, that context now shows up in crash dumps, SIGQUIT traces, and runtime.Stack output too (handy for telling apart otherwise identical goroutines).
Here we attach a label, then dump the current goroutine’s stack to see it in action:
ctx := context.Background() pprof.Do(ctx, pprof.Labels(“request”, “42″), func(ctx context.Context) { buf := make([]byte, 1<<12) n := runtime.Stack(buf, false) fmt.Printf(“%s”, buf[:n]) })
goroutine 1 [running] {request: 42}: main.main.func1(…) …/main.go:14 +0x38 runtime/pprof.Do(…) …/runtime/pprof/runtime.go:57 +0x8c main.main() …/main.go:12 +0x6c
The pointer arguments, offsets, and file paths differ from run to run; what’s new is the {request: 42} appended right after the goroutine’s [running] state: its pprof labels. That same {…} annotation appears on the header of every labeled goroutine in a panic or SIGQUIT traceback. You can disable it with GODEBUG=tracebacklabels=0 (the setting was added in Go 1.26). The opt-out is expected to stay indefinitely, in case labels carry sensitive data you don’t want in tracebacks.
𝗗 runtime/pprof
𝗣 76349
𝗖𝗟 3694f33, 19c994c
𝗔 David Finkel
Goroutine leak profile
#
Go 1.26 introduced a goroutine leak detector as an experiment. In Go 1.27 it graduates to a regular profile: runtime/pprof exposes a goroutineleak profile that runs a GC cycle to find goroutines that are permanently blocked (leaked) and reports their stacks; no GOEXPERIMENT needed anymore.
A “leaked” goroutine is one blocked forever on a channel, mutex, or similar, with no way to ever make progress. The classic example is a goroutine that sends to a channel it alone holds, so nobody can ever receive from it:
func leak() { ch := make(chan int) // only this goroutine ever sees ch ch <- 1 // blocks forever: nobody will ever receive }
Start one, let it park, then dump the profile:
go leak() // this goroutine can never finish
runtime.Gosched() // let it park on the send
// The GC-backed scan finds goroutines that can never make progress. pprof.Lookup(“goroutineleak”).WriteTo(os.Stdout, 1)
goroutineleak profile: total 1 1 @ 0x… 0x… 0x… 0x… 0x… # 0x… main.leak+0x27 …/main.go:11
The total 1 line says the detector found exactly one leaked goroutine, and the stack pins it to main.leak: the ch <- 1 send that will never complete (the addresses vary from run to run). In a real service you’d usually scrape the /debug/pprof/goroutineleak net/http/pprof endpoint instead of writing to stdout.
𝗗 runtime/pprof
𝗣 74609
𝗖𝗟 253aa2a, 1644917, afcf04c
𝗔 Vlad Saioc, Austin Clements, Cherry Mui
Post-quantum signatures
#
The new crypto/mldsa package implements ML-DSA, the post-quantum digital signature scheme specified in FIPS 204. It comes in three parameter sets (MLDSA44, MLDSA65, and MLDSA87), trading key/signature size for security level.
priv, _ := mldsa.GenerateKey(mldsa.MLDSA65())
msg := []byte(“victoria metrics”) 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)
scheme: ML-DSA-65 sig size: 3309 verified: true
ML-DSA support also reaches crypto/x509 (private keys, public keys, and signatures) and crypto/tls (the new MLDSA44, MLDSA65, and MLDSA87 signature schemes in TLS 1.3).
𝗗 crypto/mldsa
𝗣 77626
𝗖𝗟 7bc111c
𝗔 Filippo Valsorda, Daniel McCarney
The uuid package
#
Go finally has a UUID package in the standard library. The new top-level uuid package generates and parses UUIDs per RFC 9562, using a cryptographically secure random source. Random-component UUIDs are comparable, so you can use == on them directly.