golang

Go 1.22: Интерактивные заметки к релизу

  • пятница, 9 февраля 2024 г. в 00:00:21
https://habr.com/ru/articles/792242/

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/v2math/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