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 variablesRange over integersNew random packageHTTP routingSlicesVersion handlingOther stdlib changesTooling and runtimeSummary

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.

𝗣 60078 • 𝗖𝗟 532580

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.

𝗣 61405 • 𝗖𝗟 510538, 538718

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]

𝗣 56353 • 𝗖𝗟 504882

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]

𝗣 63393 • 𝗖𝗟 543335

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]

𝗣 63913 • 𝗖𝗟 540155

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

𝗣 62039 • 𝗖𝗟 538895

Other stdlib changes

Please refer to the full release notes for details:

Tooling and runtime

Tools

Go command:

  • 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 legacy GOPATH mode
  • go mod init no longer attempts to import module requirements from configuration files for other vendoring tools (such as Gopkg.lock).
  • go test -cover now prints coverage summaries for covered packages that do not have their own test files.

Trace:

  • 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:

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:

FeatureProposalChanges
Loop variables60078532580
Range over integers61405510538, 538718
New random package61716502495, 502497, 502498, 502499, 502500, 502505, 502506, 516857
HTTP routing patterns61410526815, 526616, 528355, 530575, 530479, 530481, 530461, 552195
Version handling62039538895

And minor ones:

FeatureProposalChanges
Slices: concat56353504882
Slices: shrink63393543335
Slices: insert63913504882

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.