Go 1.22: Interactive release notes
Go 1.22 is out, so it's a good time to explore what's new. The official release notes are pretty dry, so I prepared an interactive version with lots of examples showing what has changed and what the new behavior is.
Read on and see!
Loop variables • Range over integers • New random package • HTTP routing • Slices • Version handling • Other stdlib changes • Tooling and runtime • Summary
I also provide links to the proposals (𝗣) and commits (𝗖𝗟) for the features described. Check them out for motivation and implementation details.
No more sharing of loop variables
Previously, the variables declared by a "for" loop were created once and updated by each iteration. This led to common mistakes such as the loop-goroutine one:
// go 1.21
values := []int{1, 2, 3, 4, 5}
for _, val := range values {
go func() {
fmt.Printf("%d ", val)
}()
}
5 5 5 5 5
In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs:
// go 1.22
values := []int{1, 2, 3, 4, 5}
for _, val := range values {
go func() {
fmt.Printf("%d ", val)
}()
}
5 1 2 3 4
The new for-loop semantics is only enabled with a go.mod
specifying the version 1.22
or later.
Range over integers
"For" loops may now range over integers:
for i := range 10 {
fmt.Print(10 - i, " ")
}
fmt.Println()
fmt.Println("go1.22 has lift-off!")
10 9 8 7 6 5 4 3 2 1
go1.22 has lift-off!
See the spec for details.
New random package
Go 1.22 includes the first "v2" package in the standard library, math/rand/v2
. The changes compared to math/rand
are detailed in proposal #61716.
The most important changes are:
No Read method
The Read
method, deprecated in math/rand
, was not carried forward for math/rand/v2
(it remains available in math/rand
). The vast majority of calls to Read
should use crypto/rand
's Read
instead:
package main
import (
"crypto/rand"
"fmt"
)
func main() {
b := make([]byte, 5)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("5 random bytes: %v\n", b)
}
5 random bytes: [245 181 23 109 149]
Otherwise a custom Read
can be constructed using the Uint64
method:
package main
import (
"fmt"
"math/rand/v2"
)
func Read(p []byte) (n int, err error) {
for i := 0; i < len(p); {
val := rand.Uint64()
for j := 0; j < 8 && i < len(p); j++ {
p[i] = byte(val & 0xff)
val >>= 8
i++
}
}
return len(p), nil
}
func main() {
b := make([]byte, 5)
Read(b)
fmt.Printf("5 random bytes: %v\n", b)
}
5 random bytes: [135 25 55 202 33]
Generic N-function
The new generic function N
is like Int64N
or Uint64N
but works for any integer type:
{
// random integer
var max int = 100
n := rand.N(max)
fmt.Println("integer n =", n)
}
{
// random unsigned integer
var max uint = 100
n := rand.N(max)
fmt.Println("unsigned int n =", n)
}
integer n = 55
unsigned int n = 96
Works for durations too (since time.Duration
is based on int64
):
// random duration
max := 100*time.Millisecond
n := rand.N(max)
fmt.Println("duration n =", n)
duration n = 78.949532ms
Fixed naming
Top-level functions and methods from math/rand
:
Intn Int31 Int31n Int63 Int64n
are spelled more idiomatically in math/rand/v2
:
IntN Int32 Int32N Int64 Int64N
fmt.Println("IntN =", rand.IntN(100))
fmt.Println("Int32 =", rand.Int32())
fmt.Println("Int32N =", rand.Int32N(100))
fmt.Println("Int64 =", rand.Int64())
fmt.Println("Int64N =", rand.Int64N(100))
IntN = 48
Int32 = 925068909
Int32N = 11
Int64 = 4225327687323893784
Int64N = 73
There are also new top-level functions and methods:
UintN Uint32 Uint32N Uint64 Uint64N
fmt.Println("UintN =", rand.UintN(100))
fmt.Println("Uint32 =", rand.Uint32())
fmt.Println("Uint32N =", rand.Uint32N(100))
fmt.Println("Uint64 =", rand.Uint64())
fmt.Println("Uint64N =", rand.Uint64N(100))
UintN = 46
Uint32 = 2549858040
Uint32N = 97
Uint64 = 3964182289933687247
Uint64N = 9
And more
The global generator accessed by top-level functions is unconditionally randomly seeded. Because the API guarantees no fixed sequence of results, optimizations like per-thread random generator states are now possible.
Many methods now use faster algorithms that were not possible to adopt in math/rand
because they changed the output streams.
The Mitchell & Reeds LFSR generator provided by math/rand
's Source
has been replaced by two more modern pseudo-random generator sources:
ChaCha8
and PCG
. ChaCha8 is a new, cryptographically strong random number generator
roughly similar to PCG in efficiency.
ChaCha8 is the algorithm used for the top-level functions in math/rand/v2
. As of Go 1.22, math/rand
's top-level functions (when not explicitly seeded) and the Go runtime also use ChaCha8 for randomness.
The Source
interface now has a single Uint64
method; there is no Source64
interface.
𝗣 61716 • 𝗖𝗟 502495, 502497, 502498, 502499, 502500, 502505, 502506, 516857
Enhanced routing patterns
HTTP routing in the standard library is now more expressive. The patterns used by net/http.ServeMux
have been enhanced to accept methods and wildcards.
Registering a handler with a method, like POST /items/create
, restricts invocations of the handler to requests with the given method. A pattern with a method takes precedence over a matching pattern without one:
mux.HandleFunc("POST /items/create", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "POST item created")
})
mux.HandleFunc("/items/create", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "item created")
})
{
// uses POST route
resp, _ := http.Post(server.URL+"/items/create", "text/plain", nil)
body, _ := io.ReadAll(resp.Body)
fmt.Println("POST /items/create:", string(body))
resp.Body.Close()
}
{
// uses generic route
resp, _ := http.Get(server.URL+"/items/create")
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /items/create:", string(body))
resp.Body.Close()
}
POST /items/create: POST item created
GET /items/create: item created
As a special case, registering a handler with GET
also registers it with HEAD
.
Wildcards in patterns, like /items/{id}
, match segments of the URL path. The actual segment value may be accessed by calling the Request.PathValue
method:
mux.HandleFunc("/items/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "Item ID = %s", id)
})
req, _ := http.NewRequest("GET", server.URL+"/items/12345", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /items/12345:", string(body))
resp.Body.Close()
GET /items/{id}: Item ID: 12345
A wildcard ending in ...
, like /files/{path...}
, must occur at the end of a pattern and matches all the remaining segments:
mux.HandleFunc("/files/{path...}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "File path = %s", path)
})
req, _ := http.NewRequest("GET", server.URL+"/files/a/b/c", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /files/a/b/c:", string(body))
resp.Body.Close()
GET /files/{path...}: File path: a/b/c
A pattern that ends in /
matches all paths that have it as a prefix, as always. To match the exact pattern including the trailing slash, end it with {$}
, as in /exact/match/{$}
:
mux.HandleFunc("/exact/match/{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "exact match")
})
mux.HandleFunc("/exact/match/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "prefix match")
})
{
// exact match
req, _ := http.NewRequest("GET", server.URL+"/exact/match/", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /exact/match/:", string(body))
resp.Body.Close()
}
{
// prefix match
req, _ := http.NewRequest("GET", server.URL+"/exact/match/123", nil)
resp, _ := http.DefaultClient.Do(req)
body, _ := io.ReadAll(resp.Body)
fmt.Println("GET /exact/match/123:", string(body))
resp.Body.Close()
}
GET /exact/match/: exact match
GET /exact/match/123: prefix match
If two patterns overlap in the requests that they match, then the more specific pattern takes precedence. If neither is more specific, the patterns conflict. This rule generalizes the original precedence rules and maintains the property that the order in which patterns are registered does not matter.
This change breaks backwards compatibility in small ways, some obvious — patterns with "{" and "}" behave differently — and some less so — treatment of escaped paths has been improved. The change is controlled by a GODEBUG
field named httpmuxgo121
. Set httpmuxgo121=1
to restore the old behavior.
𝗣 61410 • 𝗖𝗟 526815, 526616, 528355, 530575, 530479, 530481, 530461, 552195
Slices
The new function Concat
concatenates multiple slices:
s1 := []int{1, 2}
s2 := []int{3, 4}
s3 := []int{5, 6}
res := slices.Concat(s1, s2, s3)
fmt.Println(res)
[1 2 3 4 5 6]
Functions that shrink the size of a slice (Delete
, DeleteFunc
, Compact
, CompactFunc
, and Replace
) now zero the elements between the new length and the old length (see proposal #63393 for the reasoning).
Old behavior (note the src
value after Delete
):
// go 1.21
src := []int{11, 12, 13, 14}
// delete #1 and #2
mod := slices.Delete(src, 1, 3)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
src: [11 14 13 14]
mod: [11 14]
New behavior:
// go 1.22
src := []int{11, 12, 13, 14}
// delete #1 and #2
mod := slices.Delete(src, 1, 3)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
src: [11 14 0 0]
mod: [11 14]
Compact
example:
src := []int{11, 12, 12, 12, 15}
mod := slices.Compact(src)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
src: [11 12 15 0 0]
mod: [11 12 15]
And Replace
one:
src := []int{11, 12, 13, 14}
// replace #1 and #2 with 99
mod := slices.Replace(src, 1, 3, 99)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
src: [11 99 14 0]
mod: [11 99 14]
Insert
now always panics if the argument i
is out of range. Previously it did not panic in this situation if there were no elements to be inserted:
// go 1.21
src := []string{"alice", "bob", "cindy"}
// we are not actually inserting anything,
// so don't panic
mod := slices.Insert(src, 4)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
src: [alice bob cindy]
mod: [alice bob cindy]
But now it panics:
// go 1.22
src := []string{"alice", "bob", "cindy"}
// we are not actually inserting anything,
// but it panics anyway because 4 is out of range
mod := slices.Insert(src, 4)
fmt.Println("src:", src)
fmt.Println("mod:", mod)
panic: runtime error: slice bounds out of range [4:3]
Version handling
The new go/version
package implements functions for validating and comparing Go version strings.
Compare
returns -1, 0, or +1 depending on whether x < y, x == y, or x > y, interpreted as Go versions:
fmt.Println(version.Compare("go1.22", "go1.21"))
1
IsValid
reports whether the version x is valid:
fmt.Println(version.IsValid("go1.22"))
fmt.Println(version.IsValid("1.22"))
true
false
Other stdlib changes
Please refer to the full release notes for details:
Tooling and runtime
Tools
- Commands in workspaces can now use a
vendor
directory containing the dependencies of the workspace. go get
is no longer supported outside of a module in the legacyGOPATH
modego mod init
no longer attempts to import module requirements from configuration files for other vendoring tools (such asGopkg.lock
).go test -cover
now prints coverage summaries for covered packages that do not have their own test files.
- The
trace
tool's web UI has been gently refreshed as part of the work to support the new tracer, resolving several issues and improving the readability of various sub-pages.
Vet:
- References to loop variables
- New warnings for missing values after append
- New warnings for deferring time.Since
- New warnings for mismatched key-value pairs in log/slog calls
Runtime
The runtime now keeps type-based garbage collection metadata nearer to each heap object, improving the CPU performance (latency or throughput) of Go programs by 1–3%.
This change also reduces the memory overhead of the majority Go programs by approximately 1% by deduplicating redundant metadata.
Compiler
Profile-guided Optimization (PGO) builds can now devirtualize a higher proportion of calls than previously possible. Most programs from a representative set of Go programs now see between 2 and 14% improvement from enabling PGO.
Summary
Go 1.22 finally fixes an unfortunate design decision about loop variables that bit thousands of developers while they were learning the language. It also adds some nice syntactic sugar for iterating over integers, a new shiny package for working with random numbers, and long-awaited pattern-based HTTP routing. And a ton of other improvements, of course.
Here are the major features we covered:
And minor ones:
Feature | Proposal | Changes |
---|---|---|
Slices: concat | 56353 | 504882 |
Slices: shrink | 63393 | 543335 |
Slices: insert | 63913 | 504882 |
All in all, a great release!
──
P.S. Interactive examples in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.
★ Subscribe to keep up with new posts.