Go 1.22: Интерактивные заметки к релизу
- пятница, 9 февраля 2024 г. в 00:00:21

Вчера вышел 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 при сборке.
Первый в истории v2-пакет в стандартной библиотеке: math/rand/v2. Подробности изменений относительно math/rand описаны в спецификации #61716. Команда Go планирует сделать утилиту для миграции на новую версию чуть позже (вероятно, в Go 1.23).
Вот основные изменения:
Метод 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 похожа на 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 Int64Nfmt.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 Uint64Nfmt.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:
vendor для зависимостей.go get больше не поддерживается вне модуля в устаревшем режиме GOPATH.go mod init больше не пытается импортировать зависимости из файлов конфигурации других инструментов управления зависимостями (вроде Gopkg.lock).go test -cover теперь показывает покрытие кода для пакетов, у которых нет собственных тестовых файлов.Утилита trace: улучшили веб-интерфейс.
Утилита vet:
Среда исполнения теперь хранит метаданные о типе (для сборки мусора) ближе к каждому объекту кучи. Это улучшает производительность CPU (latency или пропускную способность) программ на 1–3%.
Заодно это снижает накладные расходы на память для большинства программ примерно на 1% (за счет не-дублирования избыточных метаданных).
Оптимизация по профилю (PGO) теперь может девиртуализировать больше вызовов, чем раньше. Большинство программ из репрезентативной выборки показали улучшение 2-14% после включения PGO.
Go 1.22 наконец-то исправил досадную проблему с переменными цикла, от которой пострадало не одно поколение новоиспеченных Go-разработчиков. Новый релиз также подсыпал синтаксического сахара для итерирования по числам, принес новый пакет для генерации случайных чисел, и добавил долгожданные шаблоны в HTTP-роутинг. И принес еще тонну мелких улучшений, конечно.
В целом, отличный релиз!
Если хотите больше интересных штук на Go — подписывайтесь на мой канал @thank_go