Тур по Go 1.23
Go 1.23 уже вышел, так что сейчас самое время изучить, что нового. Официальные заметки к релизу сухие как прошлогодние сухари, поэтому я подготовил более живую версию с обзором некоторых новых функций.
range по функциям
В Go 1.23 появился цикл range по функциям. Проще всего показать на примере.
Предположим, вы написали собственный тип OrderedMap, который (в отличие от обычной карты) сохраняет порядок элементов.
Сделали ему конструктор и метод Set:
m := NewOrderedMap[string, int]()
m.Set("one", 1)
m.Set("two", 2)
m.Set("thr", 3)
Хорошо, а как теперь итерироваться по карте? Традиционно это делали примерно так:
m.Range(func(k string, v int) {
fmt.Println(k, v)
})
// one 1
// two 2
// thr 3
А с новой фичей range-over-func можно сделать так:
for k, v := range m.Range {
fmt.Println(k, v)
}
То есть это такой синтаксический сахарок.
Пакет unique
Пакет unique (Go 1.23+) помогает сэкономить память, если обрабатываются неуникальные значения.
Рассмотрим пример. У нас есть генератор слов:
const nDistinct = 100
const wordLen = 40
generate := wordGen(nDistinct, wordLen)
fmt.Println(generate())
// nlfgseuif...
fmt.Println(generate())
// anixapidn...
fmt.Println(generate())
// czedtcbxa...
Размер словаря ограничен (100 слов), так что генерируемые значения будут часто повторяться.
Сгенерим 10000 слов и запишем их в срез строк:
words = make([]string, nWords)
for i := range nWords {
words[i] = generate()
}
Memory used: 622 KB
10К слов заняли 600 Кб в куче.
Попробуем другой подход. Используем unique.Handle, чтобы назначить дескриптор каждому уникальному слову, и будем хранить эти дескрипторы вместо самих слов:
words = make([]unique.Handle[string], nWords)
for i := range nWords {
words[i] = unique.Make(generate())
}
Memory used: 95 KB
100 Кб вместо 600 Кб — в 6 раз меньше памяти.
Функция Make создает уникальный дескриптор для значения любого comparable-типа. Она возвращает ссылку на «каноническую» копию значения в виде объекта Handle.
Два Handle равны только в том случае, если равны исходные значения. Сравнение двух Handle эффективно, потому что сводится к сравнению указателей.
Под капотом пакет unique ведет глобальный конкурентно-безопасный кеш всех добавленных значений, гарантируя их уникальность и повторное использование.
Таймеры
Это прям детективная история. В Go есть таймер (тип Timer), а в нем — поле с каналом (Timer.C), в который таймер тикает спустя указанное время.
В коде стдлибы таймер создается так:
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
// ...
}
startTimer(&t.r)
return t
}
Такая реализация привела к проблемам с time.After и Reset, от которых многие страдали.
И вот в Go 1.23 решили это исправить, для чего сделали канал в таймере небуферизованным:
// As of Go 1.23, the channel is synchronous (unbuffered, capacity 0),
// eliminating the possibility of those stale values.
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := (*Timer)(newTimer(when(d), 0, sendTime, c, syncTimer(c)))
t.C = c
return t
}
Вот только если вы посмотрите на фактический код, то канал-то остался буферизованным 😁
c := make(chan Time, 1)
Из комментариев к коммиту выясняется, что канал действительно остался буферизованным, но притворяется, что никакого буфера у него нет:
Specifically, the timer channel has a 1-element buffer like it always has, but len(t.C) and cap(t.C) are special-cased to return 0 anyway, so user code cannot see what's in the buffer except with a receive.
Эту логику вкорячили прямо в реализацию канала (тип chan).
Что тут скажешь. Бывает :)
http.Cookie
Давайте поговорим об http-куках. Если вы думаете, что это такие пары «ключ-значение», которые сервер и клиент гоняют туда-сюда в каждом запросе, то это верно лишь отчасти.
За годы развития веба куки обзавелись дополнительной атрибутикой. Вот как выглядит структура Cookie в Go 1.23:
NameиValue— ключ и значение.Quoted— true, если значение передано в кавычках.Path— разрешает куку на страницах, которые начинаются с указанного пути.Domain— разрешает куку на указанном домене и всех его поддоменах.Expires— дата окончания годности куки.MaxAge— срок жизни куки в секундах.Secure— разрешает куку только по HTTPS.HttpOnly— закрывает доступ к куке из JavaScript.SameSite— разрешает или запрещает куку при кросс-доменных запросах.Partitioned— ограничивает доступ к third-party кукам.
Неслабо, да?
Начиная с версии Go 1.23, серверную куку можно распарсить из строки с помощью http.ParseSetCookie:
line := "session_id=abc123; SameSite=None; Secure; Partitioned; Path=/; Domain=example.com"
cookie, err := http.ParseSetCookie(line)
Браузерные куки тоже можно распарсить из строки, с помощью http.ParseCookie:
line := "session_id=abc123; dnt=1; lang=en; lang=de"
cookies, err := http.ParseCookie(line)
os.CopyFS
Раньше, чтобы рекурсивно скопировать каталог со всем содержимым, вам пришлось бы написать 50 строк кода.
Теперь, благодаря os.CopyFS в Go 1.23, будет достаточно одной:
src := os.DirFS("/home/src")
dst := "/home/dst"
err := os.CopyFS(dst, src)
Вроде и мелочь, но весьма уместная.
Заключение
В Go 1.23 заработали итераторы и range по функциям, появился полезный пакет unique для экономии памяти, исправили старую проблему со сбросом таймеров и улучшили работу с куками. В целом — отличный релиз!
★ Подписывайтесь на канал и проходите курсы.