Tested with Go 1.27.3 (golang:1.27.3, arm64) · verified

Updated: 2026-10-27

Go 1.22, released on 6 February 2024, brought two changes that still define idiomatic Go in 2026: each iteration of a for loop gets its own variable, and net/http.ServeMux routes by method and with wildcards. Version 1.22 itself is no longer supported, because Go only maintains the two latest major releases, which on 27 October 2026 are 1.27 and 1.26. Those two changes, however, remain, and later releases have built on them. This guide checks both with Go 1.27.3 and notes what changed since. It is also available in Spanish.

Key takeaways

  • Since Go 1.22, a for loop variable is created anew on each iteration, but only in modules whose go.mod declares go 1.22 or later.
  • With Go 1.27.3, the same loop prints c c c with go 1.21 in go.mod and three different letters with go 1.22.
  • go fix, rebuilt in Go 1.26, removes the v := v copies that the new semantics made unnecessary.
  • ServeMux patterns accept a method, named wildcards such as {id} and remainder wildcards such as {path...}, and answer 405 with an Allow header when the method does not match.
  • Go 1.23 added Request.Pattern, Go 1.26 changed trailing-slash redirects from 301 to 307, and GODEBUG=httpmuxgo121=1 still restores the old router in Go 1.27.3.

What changed in loop variables?

Up to Go 1.21, the variable declared in a for header was a single one for the whole loop. Every closure or goroutine created inside captured that same variable, not its value on that pass, and when it ran later it saw the last value. The Go team describes the problem in Fixing For Loops in Go 1.22[1], where it cites a public Let’s Encrypt incident[2] caused by this capture.

This program starts one goroutine per element and each one prints its own:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for _, v := range []string{"a", "b", "c"} {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Print(v, " ")
        }()
    }
    wg.Wait()
    fmt.Println()
}

We ran it on 27 October 2026 with the golang:1.27.3 image, three times with each go directive in go.mod. With go 1.21, all three runs printed c c c. With go 1.22, all three printed c a b: each goroutine saw its own letter, in the order the scheduler ran them.

The result shows the rule that makes the transition safe. The new semantics depend on the module’s go directive, not on the compiler version, so a current toolchain still compiles code that declares an older version with the old semantics. Before shipping it, Google forced the new semantics on all its internal builds from May 2023. According to the same article, it saw no problems in production code over four months, though it did have to fix tests that were not checking what they thought.

How do you remove the v := v copies?

With the new semantics, the defensive v := v copy inside the loop is redundant. Go 1.26 rebuilt go fix as the home of the modernizers, according to its release notes[3], and one of them removes those copies in modules that already declare go 1.22 or later. On a loop with the copy, go fix -diff ./... in Go 1.27.3 proposed this change:

    for _, v := range []int{1, 2, 3} {
-       v := v
        fns = append(fns, func() { fmt.Print(v) })
    }

Without -diff, go fix ./... applies the change to the files. Review it before committing if any copy was intentional.

Which patterns does ServeMux accept?

The second Go 1.22 change is in net/http.ServeMux. Up to 1.21 the standard multiplexer only matched path prefixes, with no methods or parameters, and any REST API reached for gorilla/mux or chi. Since 1.22 a pattern can carry the method in front, named segments in braces and a trailing wildcard that collects the rest of the path. The article Routing Enhancements for Go 1.22[4] explains it and the ServeMux documentation[5] has the details.

This program registers two routes and sends three requests to the multiplexer with httptest, without opening any port:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "user %s (%s)", r.PathValue("id"), r.Pattern)
    })
    mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "file %s", r.PathValue("path"))
    })
    for _, req := range []struct{ m, p string }{
        {"GET", "/users/42"}, {"POST", "/users/42"}, {"GET", "/files/a/b.txt"},
    } {
        rec := httptest.NewRecorder()
        mux.ServeHTTP(rec, httptest.NewRequest(req.m, req.p, nil))
        fmt.Println(req.m, req.p, rec.Code, rec.Header().Get("Allow"), rec.Body.String())
    }
}

With Go 1.27.3, the output was this:

GET /users/42 200  user 42 (GET /users/{id})
POST /users/42 405 GET, HEAD Method Not Allowed
GET /files/a/b.txt 200  file a/b.txt

The second line saves the most work. The multiplexer knows the path exists for another method, answers 405 and fills the Allow header with GET, HEAD, because a GET pattern also serves HEAD. The {path...} wildcard collects a/b.txt including its slash, and r.PathValue reads the values without parsing the URL by hand.

What has changed in ServeMux since Go 1.22?

Later releases have polished the router without changing its syntax. These are the changes that affect code already using it:

Version Date net/http change
Go 1.23 2024-08-13 Request.Pattern holds the matched pattern
Go 1.23 2024-08-13 Spaces or tabs allowed after the method
Go 1.25 2025-08-12 CrossOriginProtection against CSRF
Go 1.26 2026-02-10 Trailing-slash redirect uses 307

The Go 1.26 change is the only one that alters existing responses. According to the Go 1.26 release notes[3], a request for /docs against a /docs/ pattern now gets a 307 instead of a 301, and that is what Go 1.27.3 answered in our test. Request.Pattern is handy for metrics and traces, because it labels each request with its route rather than the concrete URL. And if a migration needs the pre-1.22 behaviour, GODEBUG=httpmuxgo121=1 restores it: with that variable, the same program answered 404 to all three requests on Go 1.27.3.

For a small or medium REST service, the standard multiplexer is enough. An external router still makes sense when you need route groups with shared middleware, validation or OpenAPI generation.

What else did Go 1.22 bring and where did it go?

The rest of Go 1.22’s changes still stand too, and two of them paved the way for bigger ones:

  • for i := range 10: ranging over integers arrived in 1.22, and 1.23 extended it to ranging over iterator functions, the basis of the iterators in slices, maps and, in 1.26, reflect.
  • math/rand/v2: it was the first /v2 package in the standard library, with the ChaCha8 generator and functions such as IntN.
  • The toolchain directive: it arrived in 1.21 and lets go download the compiler the module asks for; with it, 1.26 and 1.27 coexist on the same machine without trouble.

If you work with generics, the guide on generics in Go three years later covers what is left of them in real code.

How do you upgrade a pre-1.22 module today?

A module that still declares go 1.21 or earlier compiles with Go 1.27, but keeps the old loop semantics. To move it to the new ones, follow these steps in order:

  1. Raise the directive with go mod edit -go=1.26, the oldest supported release, so you do not force 1.27 on whoever builds the module.
  2. Run go vet ./... and go test ./...: a test that depended on the shared variable will fail here.
  3. Run go fix -diff ./..., review the changes and apply them with go fix ./....

Frequently asked questions

Does the new loop behaviour turn on just by installing a recent Go?

No. The per-iteration variable depends on the go directive in go.mod, not on the compiler. With Go 1.27.3 and go 1.21 in go.mod, our test program kept printing c c c; after changing the directive to go 1.22, each goroutine printed its own letter.

Can I drop gorilla/mux or chi and use ServeMux alone?

For a small or medium REST service, yes. Since Go 1.22, ServeMux accepts the method in the pattern, named wildcards read with r.PathValue and remainder wildcards with {name...}, and it answers 405 with an Allow header when the method does not match. An external router adds value with route groups, per-group middleware, validation or OpenAPI generation.

Is Go 1.22 still supported?

No. Go maintains each major release until two newer ones ship, so 1.22 lost support when 1.24 arrived on 11 February 2025. On 27 October 2026 the supported releases are 1.27 (latest, 1.27.3) and 1.26 (latest, 1.26.10).

Conclusion

Of everything Go 1.22 brought, two changes still shape how Go is written in 2026. The per-iteration loop variable removed a whole class of goroutine bugs, and the ServeMux with methods and wildcards left external routers for the cases that need them. Later releases have polished both without breaking them, and go fix now cleans up what is left of the old semantics. If you maintain a module that still declares go 1.21, raising the directive is the most rewarding part of the whole migration.

Sources: [1] Go 1.22 Release Notes[6], [2] Fixing For Loops in Go 1.22, The Go Blog[1], [3] Routing Enhancements for Go 1.22, The Go Blog[4], [4] Go 1.26 Release Notes[3], [5] Go release history[7].

Sources

  1. Fixing For Loops in Go 1.22
  2. public Let’s Encrypt incident
  3. release notes
  4. Routing Enhancements for Go 1.22
  5. ServeMux documentation
  6. Go 1.22 Release Notes
  7. Go release history