
Вчера вышел 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
