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