Go 1.22: Интерактивные заметки к релизу
Вчера вышел Go 1.22, и многие новые фичи можно попробовать прямо из браузера. Давайте пройдемся по ним!
Хабр не разрешает встраивать интерактивные примеры кода в статью, поэтому я сделал их внешними ссылками.
Починили счетчик цикла в замыканиях
Раньше переменные, объявленные в цикле for, создавались один раз и обновлялись на каждой итерации. Это приводило к ошибкам вроде использования счетчика в горутинах:
// 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
Запустить ▶
В Go 1.22 каждая итерация цикла создает новые переменные, так что багов больше не будет:
// 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
Запустить ▶
Изменение обратно совместимо: новая семантика заработает, только если указать версию 1.22 в go.mod
. Так что старый код продолжит работать без изменений, пока вы сами не захотите его обновить.
Итерирование по числам
Цикл for range теперь умеет итерироваться по диапазону целых чисел:
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!
Запустить ▶
Смотрите подробности в спецификации.
Кроме того, можно попробовать экспериментальное итерирование по функциям. Для этого включите флаг GOEXPERIMENT=rangefunc
при сборке.
Новый пакет math/rand/v2
Первый в истории v2-пакет в стандартной библиотеке: math/rand/v2
. Подробности изменений относительно math/rand
описаны в спецификации #61716. Команда Go планирует сделать утилиту для миграции на новую версию чуть позже (вероятно, в Go 1.23).
Вот основные изменения:
Больше нет метода Read
Метод Read
, объявленный устаревшим еще в math/rand
, не пережил перехода на math/rand/v2
(в math/rand
он по-прежнему доступен). В большинстве случаев, вместо него следует использовать Read
в пакете crypto/rand
:
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]
Запустить ▶
Либо можно сделать собственный Read
на базе метода Uint64
:
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]
Запустить ▶
Обобщенная N-функция
Новая дженерик-функция N
похожа на Int64N
или Uint64N
, но работает с любым целочисленным типом:
{
// 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
Запустить ▶
Работает и с time.Duration
тоже (он ведь основан на int64
):
// random duration
max := 100*time.Millisecond
n := rand.N(max)
fmt.Println("duration n =", n)
duration n = 78.949532ms
Запустить ▶
Исправлены названия
Функции и методы из math/rand
:
Intn Int31 Int31n Int63 Int64n
привели к принятому в Go виду в 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
Запустить ▶
Заодно добавили новые функции и методы:
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
Запустить ▶
И еще всякое
Глобальный генератор случайных чисел, доступный из функций пакета, теперь принудительно инициализируется случайным образом. Поскольку API гарантирует нефиксированную последовательность результатов, стали возможны оптимизации вроде изолированного состояния генератора для каждого потока.
Многие методы теперь используют более быстрые алгоритмы, которые не получалось применить в math/rand
, поскольку они изменяют выходные потоки.
Генератор LFSR (Mitchell & Reeds LFSR), который использовался в Source
, заменили на два более современных: ChaCha8
and PCG
. ChaCha8 — это новый криптографически стойкий генератор случайных чисел, примерно сопоставимый по эффективности с PCG.
ChaCha8 используется для функций пакета в math/rand/v2
. Функции пакета math/rand
теперь тоже используют его, если явно не инициализировать генератор. Сама среда исполнения Go тоже использует ChaCha8.
Интерфейс Source
теперь имеет единственный метод Uint64
; интерфейса Source64
больше нет.
Улучшенные шаблоны роутинга
HTTP-машрутизация в стандартной библиотеке стала более выразительной. Шаблоны, которые использует net/http.ServeMux
, теперь допускают HTTP-методы и переменные.
Если указать метод (например, POST /items/create
), то соответствующий обработчик будет вызван только для запроса с этим методом. Шаблон с методом имеет больший приоритет, чем шаблон без него:
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
Запустить ▶
Особенность: регистрация обработчика для GET
заодно включает его и для HEAD
.
Переменные в шаблонах (например, /items/{id}
) выделяют соответствующие сегменты URL. Значение переменной можно получить через метод Request.PathValue
:
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
Запустить ▶
В самом конце шаблона можно использовать переменную с многоточием (например, /files/{path...}
). В нее попадет «хвост» URL:
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
Запустить ▶
Если шаблон заканчивается на /
— он префиксно сработает на любом более длинном URL (так было и раньше). Чтобы шаблон работал только при полном совпадении (не префиксно) используйте {$}
(например, /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
Запустить ▶
Если под запрос подходят несколько шаблонов, используется более специфичный. Если более специфичного нет, то считается, что шаблоны конфликтуют (будет паника). Порядок определения шаблонов роли не играет (как и раньше).
Изменения в роутинге слегка ломают обратную совместимость (например, изменилась трактовка символов {}
). Можно вернуть старое поведение, задав GODEBUG
-свойство httpmuxgo121=1
.
Срезы
Новая функция Concat
объединяет несколько срезов в один:
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]
Запустить ▶
Функции, которые уменьшают размер среза (Delete
, DeleteFunc
, Compact
, CompactFunc
и Replace
) теперь обнуляют «хвост» массива под срезом (элементы между старой и новой длиной среза). Подробности и аргументация — в спецификации #63393.
Старое поведение (обратите внимание на значение src
после 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]
Запустить ▶
Новое поведение:
// 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
:
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]
Запустить ▶
Пример для Replace
:
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
теперь всегда паникует, если аргумент i
выходит за рамки среза. Раньше не паниковал, если фактически вставка не происходила:
// 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]
Запустить ▶
А теперь паникует:
// 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]
Запустить ▶
Прочие изменения
Стандартная библиотека
Множество мелочей в самых разных пакетах стандартной библиотеки.
Тулинг
Утилита go:
- Команды в рабочих пространствах (workspaces) теперь могут использовать каталог
vendor
для зависимостей. go get
больше не поддерживается вне модуля в устаревшем режимеGOPATH
.go mod init
больше не пытается импортировать зависимости из файлов конфигурации других инструментов управления зависимостями (вродеGopkg.lock
).go test -cover
теперь показывает покрытие кода для пакетов, у которых нет собственных тестовых файлов.
Утилита trace: улучшили веб-интерфейс.
Утилита vet:
- Поддержка новой логики для переменных цикла.
- Предупреждение для append без значений.
- Предупреждение для defer time.Since.
- Предупреждения для некорретных вызовов log/slog.
Среда исполнения
Среда исполнения теперь хранит метаданные о типе (для сборки мусора) ближе к каждому объекту кучи. Это улучшает производительность CPU (latency или пропускную способность) программ на 1–3%.
Заодно это снижает накладные расходы на память для большинства программ примерно на 1% (за счет не-дублирования избыточных метаданных).
Компилятор
Оптимизация по профилю (PGO) теперь может девиртуализировать больше вызовов, чем раньше. Большинство программ из репрезентативной выборки показали улучшение 2–14% после включения PGO.
Резюме
Go 1.22 наконец-то исправил досадную проблему с переменными цикла, от которой пострадало не одно поколение новоиспеченных Go-разработчиков. Новый релиз также подсыпал синтаксического сахара для итерирования по числам, принес новый пакет для генерации случайных чисел, и добавил долгожданные шаблоны в HTTP-роутинг. И принес еще тонну мелких улучшений, конечно.
В целом, отличный релиз!
Если хотите больше интересных штук на Go — подписывайтесь на мой канал @thank_go