Тур по 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 для экономии памяти, исправили старую проблему со сбросом таймеров и улучшили работу с куками. В целом — отличный релиз!

★ Подписывайтесь на канал и проходите курсы.