

Your privacy settings or an extension blocked the booking widget.
If you use a blocker, allow embeds for this site and try again.
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:
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 ツ
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.
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.
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.
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 ).
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.
a := uuid . MustParse ( "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" ) fmt . Println ( "parsed:" , a ) fmt . Println ( "nil: " , uuid . Nil ()) fmt . Println ( "max: " , uuid . Max ())
parsed: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 nil: 00000000-0000-0000-0000-000000000000 max: ffffffff-ffff-ffff-ffff-ffffffffffff For generation, uuid.New() picks an algorithm suitable for most uses, while uuid.NewV4() gives a purely random UUID and uuid.NewV7() gives a time-ordered one; the latter is great for database keys because it sorts by creation time. Each call produces a fresh value, so try running this a few times:
fmt . Println ( uuid . NewV4 ()) // random fmt . Println ( uuid . NewV7 ()) // time-ordered
The long-awaited encoding/json/v2 rewrite has been experimental since Go 1.25. In Go 1.27 the experiment graduates: encoding/json/v2 and its low-level companion encoding/json/jsontext are now available without the GOEXPERIMENT=jsonv2 build flag. The quieter but bigger change: the classic encoding/json (v1) package is now backed by the v2 implementation under the hood.
The switch is transparent: behavior is preserved (only some error-message text differs), with new options pinning v2 to v1 semantics where they’d otherwise diverge. No migration is required, and GOEXPERIMENT=nojsonv2 restores the original v1 implementation if you hit a compatibility issue.
For the common case, the v2 API mirrors v1 (the import here is json "encoding/json/v2" ):
type Point struct { X int `json:"x"` Y int `json:"y"` } data , err := json . Marshal ( Point { X : 1 , Y : 2 }) fmt . Println ( string ( data ), err )
{"x":1,"y":2} <nil> One behavior worth knowing: unlike v1, which always sorts map keys, v2 does not sort them by default; skipping the sort is faster. When you need stable map output (for golden tests, say), pass the json.Deterministic option.
Go 1.27 adds an experimental simd package: portable, vector-size-agnostic SIMD that compiles down to real hardware vector instructions where they’re available and falls back to a pure-Go emulation where they aren’t. It’s off by default; you build with GOEXPERIMENT=simd to enable it.
The types are named after their element type with an s suffix ( Int32s , Float32s , Float64s , and so on), and their width is deliberately not fixed: a Float32s might hold 4 lanes on one machine and 16 on another. You load a vector from a slice, operate on it, and store it back, letting the hardware pick the width:
a := [] float32 { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 } b := [] float32 { 10 , 20 , 30 , 40 , 50 , 60 , 70 , 80 , 90 , 100 , 110 , 120 , 130 , 140 , 150 , 160 } va := simd . LoadFloat32s ( a ) // reads exactly va.Len() lanes from a vb := simd . LoadFloat32s ( b ) sum := va . Add ( vb ) // element-wise add, many lanes in one instruction out := make ([] float32 , sum . Len ()) sum . Store ( out ) fmt . Println ( out [: 4 ])
[11 22 33 44] 𝗗 simd 𝗣 78902 𝗖𝗟 44a4be9 , 8d29cf2 , 48bf922 𝗔 David Chase , Junyang Shao , Cherry Mui Cut around the last separator # strings.Cut (from Go 1.18) splits around the first occurrence of a separator. Go 1.27 adds strings.CutLast (and bytes.CutLast ) for the last occurrence (a cleaner replacement for many LastIndex dances).
before , after , found := strings . CutLas
Hacker News
news.ycombinator.com