Go 1.22: интерактивные заметки к релизу
Release notes в Go традиционно сухие как прошлогодние сухари, а я люблю разбирать все новое на примерах.
Поэтому решил оживить заметки к релизу 1.22 (выйдет в феврале) и добавил в них интерактивных примерчиков, чтобы вы могли попробовать новые фичи уже сейчас, прямо из браузера.
https://antonz.org/go-1-22
2. Сигнатура
Функция Clone должна работать со срезами любых типов (срезы чисел, строк, структур итп), а также с типами, образованными от срезов (когда мы пишем собственный тип, используя срез в качестве базового).
То есть функция должна принимать тип S
, который либо сам по себе срез ([]E
), либо образован от среза (~[]E
), а тип элемента среза при этом не важен (E any
). На птичьем языке дженериков в Go получается S ~[]E, E any
.
Такие дела.
Довольно простая функция: разбор
Вот функция, простотой которой наслаждается Ян:
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
s
: клонирует не только сам срез, но и массив под ним (как вы помните, срез — это легковесная структура, которая сама по себе не содержит данных, а содержит ссылку на массив).Go ♡ SQLite
Тут добрый человек Riyaz Ali сделал Go-модуль с набором полезных расширений SQLite:
— кодирование/декодирование
— динамический SQL
— работа с файлами
— текстовые функции
— IP адреса
— мат. статистика
— UUID
— CSV
Если вы, как и я, неравнодушны SQLite — рекомендую!
https://github.com/riyaz-ali/sqlean.go
P.S. Небольшой анонс, чтобы два раза не писать. В сентябре возвращаюсь к курсу «Многозадачность в Go», так что если вдруг переживали за его судьбу — не переживайте ツ
🗒 Узнаем больше о Go разработке
Ребята из DevCrowd (а они же основатели классного подкаста Podlodka) проводят исследование Go-разработчиков:
- Какие навыки для go-разработчиков самые важные
- Какие инструменты используются в работе
- Как попадают в профессию и куда из нее уходят
- Полезные для развития каналы, курсы и книги
Результаты будут в открытом доступе, а значит это отличный шанс узнать больше об индустрии.
Опрос довольно большой, но интересный. Мне потребовалось 10 минут, чтобы вдумчиво его пройти.
Давайте поучаствуем!
Пройти опрос
Пока в комментариях идет битва за clear, отвечу на вопрос из посткриптума:
Зачем делать встроенные min и max вместо дженерик-функций
Ответ существует, но вам он не понравится. Процитирую авторов языка (с сокращениями):
We have gone back and forth a few times on this proposal about min/max being builtins vs being in package cmp.
There are good arguments on both sides. On the one hand, min and max are fundamental arithmetic operations much like addition, which justifies making them builtins.
On the other hand, we have generics now and it would make sense to use generics to write library code rather than make them builtins.
Even among the active Go language designers, our own personal intuitions differ on this.
полный ответ
🤷♀️
Творим историю в GoВино и женщины Обстоятельства непреодолимой силы отвлекли меня от кольцевых списков, но пришло время к ним вернуться.
На всякий случай напомню, что кольцевой список — это разновидность связанного списка, у которого последний элемент ссылается на первый (подробности в предыдущей заметке).
// голова
// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘
history := ring.New(5)
addCommand
добавляет команду в историю:func addCommand(history *ring.Ring, cmd string) *ring.Ring {
history.Value = cmd
return history.Next()
}
func echo(n int) string {
return fmt.Sprintf("echo %d", n)
}
for i := 0; i < 3; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}
// 1 2 3 <nil> <nil>
// ↑
for i := 3; i < 5; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}
// 1 2 3 4 5
// ↑
for i := 5; i < 8; i++ {
cmd := echo(i + 1)
history = addCommand(history, cmd)
}
// 6 7 8 4 5
// ↑
Do
выдаст команды от самой старой к самой новой:history.Do(func(cmd any) {
fmt.Println(cmd)
})
// echo 4
// echo 5
// echo 6
// echo 7
// echo 8
History
, но об этом в другой раз.📆 Почему такой странный формат для даты и времени?
Поступил вопрос из зала, как же можно запомнить формат даты и времени в Golang и почему он вообще такой странный?2006-01-02T15:04:05.000
Понять его логику крайне сложно, потому что мы не привыкли к тому формату даты, в котором он создавался. А вот авторы языка наоборот — для них родной формат даты, используемый в Америке. И тогда все становится на свои места, а формат запоминается по простой мнемонике.Jan 2 15:04:05 2006 MST
1 2 3 4 5 6 7
Оптимизация по профилю
Быстрота гошного кода — в значительной мере заслуга оптимизирующего компилятора (а вы думали, ваша? ха!).
Эта бездушная скотинка не просто генерит машинный код по вашим креативам, а заодно инлайнит функции, переносит объекты из кучи на стек, и выполняет другие трюки — лишь бы в итоге программа была бодрее.
Чтобы хорошо оптимизировать код, нужно понимать, какие участки часто выполняются, а какие не очень — то есть знать профиль нагрузки. Поскольку вы не пускаете компилятор в продакшен, раньше эти знания были ему недоступны.
Но начиная с Go 1.20, можно приделать к компилятору ChatGPT снять профиль нагрузки с продакшена и выдать его компилятору. Благодаря этому оптимизации станут эффективнее, а бинарник заработает на 2-4% быстрее без изменений в исходниках.
Фича называется Profile-Guided Optimization (PGO), в блоге Go есть хорошая вводная статья про нее:
https://go.dev/blog/pgo-preview
Ручное управление памятью в Go 1.20
Занятую память в Go освобождает сборщик мусора. Это одно из архитектурных решений, на которых построен язык.
Несмотря на это, в версию 1.20 протащили экспериментальный пакет arena
. Он выделяет кусок памяти, в котором сборщик мусора не работает.
Типа такого:type T struct{
// ...
}
func process() {
mem := arena.NewArena()
defer mem.Free()
for i := 0; i < 10; i++ {
obj := arena.New[T](mem)
}
}
Получив полную панамку неприятных вопросов, разработчики Go потерли любые упоминания арен из release notes (хотя сама реализация, вероятно, останется).
It is unclear when, if ever, we will pick up the idea and try to push it forward into a public API, but it's not going to happen any time soon, and we don't want users to start depending on it.
Так что подробно рассказывать про арены не буду ツ
спека для любопытных
Альтернативный подход к обработке паники (несерьезное)
Все, наверно, слышали про ChatGPT. Она неплохо умеет отвечать на всевозможные вопросы по Go, но пока не предоставляет нормальное API.
Поэтому народные умельцы «повторили» ChatGPT на предыдущем поколении нейросети — GPT-3. Я не смог пройти мимо и задал ей вопрос:
> How to recover from panic in Go?
Ответ:
Recovering from panic in Go is a process that can take some time. The best way to start is to take a step back and take some deep breaths. Focus on your breathing and try to relax your body. You can also try some relaxation techniques such as progressive muscle relaxation or guided imagery.
Additionally, you can try talking to someone you trust and confide in them about your worries. Finally, if the panic persists, it may be beneficial to reach out to a professional for counseling or treatment.
Так и отвечайте на собеседованиях!
От среза к массиву в Go 1.20
Начиная с версии 1.17, в Go можно получить указатель на массив под срезом:
s := []int{1, 2, 3}
arrp := (*[3]int)(s)
arrp[2] = 42
fmt.Println(s)
// [1 2 42]
s := []int{1, 2, 3}
arr := [3]int(s)
arr[2] = 42
fmt.Println(arr)
// [1 2 42]
fmt.Println(s)
// [1 2 3]
s := []int{1, 2, 3}
arr := *(*[3]int)(s)
Новые форматы дат в Go 1.20
В феврале выходит новая версия Go, так что пора пройтись по заметным изменениям! Начнем с моего любимого — дат.
Вы наверняка знаете, что авторы Go (все по странному стечению обстоятельств американцы) выбрали совершенно всратый уникальный формат маски для даты и времени.
Например, разбор сегодняшней даты 13.01.2023 09:30
выглядит так:
const layout = "02.01.2006 15:04"
t, _ := time.Parse(layout, "13.01.2023 09:30")
fmt.Println(t)
// 2023-01-13 09:30:00 +0000 UTC
RFC3339
(ну и RFC3339Nano
еще).const (
DateTime = "2006-01-02 15:04:05"
DateOnly = "2006-01-02"
TimeOnly = "15:04:05"
)
t, _ := time.Parse(time.DateOnly, "2023-01-13")
fmt.Println(t)
// 2023-01-13 00:00:00 +0000 UTC
Композиция атомарных операций
Люди иногда думают, что композиция атомиков волшебным образом тоже становится атомарной операцией. Но это не так.
Например, второй пример из опроса выше:
var counter atomic.Int32
func increment() {
if counter.Load()%2 == 0 {
sleep(10)
counter.Add(1)
} else {
sleep(10)
counter.Add(2)
}
}
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
increment()
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter.Load())
-race
, гонок нет. Но можно ли гарантировать, какое значение будет у counter в результате? Нет.% go run atomic-2.go
192
% go run atomic-2.go
191
% go run atomic-2.go
189
increment()
в целом — не атомарная.Пример 2
var counter atomic.Int32Читать полностью…
func increment() {
if counter.Load()%2 == 0 {
sleep(10)
counter.Add(1)
} else {
sleep(10)
counter.Add(2)
}
}
Структура проекта в Go
Любимое развлечение го-разработчиков — спорить о правильной структуре проекта. Вот и авторы Go наконец решили высказаться (очень вовремя, всего-то 6 лет прошло с момента появления модулей).
Вкратце:
— Для мелких проектов — плоская структура без каталогов.
— Для более крупных — каталог internal с вложенными пакетами, публичные пакеты только при необходимости.
— Каталог cmd — когда в проекте несколько команд.
Что делать с прочими артефактами помимо кода (вроде документации и инфраструктуры) — скромно умолчали.
https://go.dev/doc/modules/layout
1. Копирование
Можно было бы вернуть просто s[:]
. Но это была бы поверхностная (shallow) копия — так скопируется срез, а массив под ним окажется тем же, что был. Поэтому изменяя копию среза, изменим и оригинальный массив.
Конструкция s[:0]
создает срез нулевой длины (zero-length), который все еще ссылается на оригинальный массив. А s[:0:0]
создает срез нулевой вместимости (zero-capacity), который ссылается на новый (пустой) массив.
Поэтому, когда мы добавляем к клону элементы оригинального s
— они попадают в новый массив. Это и обеспечивает глубокое копирование.
Довольно простая функция
Ян Ланс Тейлор (один из авторов Go) считает, что это довольно простая функция:
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
Пишем менеджер пакетов на Go
Как-то так вышло, что я написал менеджер пакетов на Go. А поскольку задачка оказалась интересной — подробно описал архитектуру и детали реализации.
Так что если вам интересно посмотреть, как темы курса Thank Go! применяются на практике — рекомендую.
Вот какие темы нашли применение в проекте:
1) Все из модуля «Основы», от базовых типов до интерфейсов и ошибок.
2) Пакеты, модули и тесты.
3) Работа с текстом.
4) Работа с файлами.
5) JSON.
6) HTTP.
Исходники тоже доступны, разумеется.
А еще там комментарии и тесты на каждый файл, благодать. Когда еще такое встретишь!
https://antonz.ru/writing-package-manager/
min, max и clear в Go 1.21
Вроде только недавно вышел Go 1.20, а тут уже релиз-кандидат 1.21 подвезли. Что поделать, будем разбирать.
В язык добавили аж три встроенных функции. Встроенные (builtin) — это такие, которые не требуют импорта пакета, вроде len
или make
.
Теперь появились еще min
, max
и clear
.
min и max
Делают ровно то, что вы от них ожидаете. Наконец-то можно не писать цикл с ручным перебором среза:
m := max(10, 3, 22)
fmt.Println(m)
// 22
m := min(10, 3, 22)
fmt.Println(m)
// 3
m := map[string]int{"one": 1, "two": 2, "three": 3}
clear(m)
fmt.Printf("%#v\n", m)
// map[string]int{}
s := []string{"one", "two", "three"}
clear(s)
fmt.Printf("%#v\n", s)
// []string{"", "", ""}
Кольцевой список
Кольцевой список — это разновидность связанного списка, у которого последний элемент ссылается на первый.
Выглядит примерно так (список из трех элементов 123, 456 и 789):
// голова
// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘
ring.Ring
.r := ring.New(3)
nil
, а голова на первом элементе:// ↓
// ┌ → <n> → <n> → <n> → ┐
// └─────────────────────┘
r.Value = 123
r = r.Next()
// ↓
// ┌ → 123 → <n> → <n> → ┐
// └─────────────────────┘
r.Value = 456
r = r.Next()
// ↓
// ┌ → 123 → 456 → <n> → ┐
// └─────────────────────┘
r.Value = 789
r = r.Next()
// ↓
// ┌ → 123 → 456 → 789 → ┐
// └─────────────────────┘
.Prev()
Джавафикация Go (несерьезное)
Несмотря на то, что я очень уважаю авторов Go, хочу заметить, что я вот на такое не подписывался:
func min[P interface{ ~int64 | ~float64 }](x, y P) P
Другие изменения в stdlib 1.20
Вот еще несколько новшеств стандартной библиотеки Go 1.20, которые меня заинтересовали.
bytes.Clone() клонирует срез байт:
b := []byte("abc")
clone := bytes.Clone(b)
s := "> go!"
s, found := strings.CutPrefix(s, "> ")
fmt.Println(s, found)
// go! true
var m sync.Map
m.Store("name", "Alice")
prev, ok := m.Swap("name", "Bob")
fmt.Println(prev, ok)
// Alice true
t1 := time.Now()
t2 := t1.Add(10 * time.Minute)
cmp := t2.Compare(t1)
fmt.Println(cmp)
// 1
Причина ошибки контекста в Go 1.20
При отмене контекста происходит ошибка context.Canceled
. Это не новость:
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println(ctx.Err())
// context canceled
context.WithCancelCause()
.cancel()
будет принимать один параметр — корневую причину ошибки (cause):ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("the night is dark"))
context.Cause()
:fmt.Println(ctx.Err())
// context canceled
fmt.Println(context.Cause(ctx))
// the night is dark
context.Cause()
? Логичнее ведь было бы добавить метод Cause
в сам контекст, аналогично методу Err
?Context
— это интерфейс. А любое изменение интерфейса ломает обратную совместимость. Поэтому сделали так.Множественные ошибки в Go 1.20
Концепция «ошибки как значения» (в противовес исключениям) получила ренессанс в современных языках вроде Go и Rust. Вы и без меня отлично это знаете, потому что в Go невозможно и шагу ступить, чтобы не споткнуться об ошибку.
Go 1.20 принес нам новую радость — комбинацию ошибок через errors.Join
:
errRaining := errors.New("it's raining")
errWindy := errors.New("it's windy")
err := errors.Join(errRaining, errWindy)
errRaining
и errWindy
. Стандартные функции errors.Is
и errors.As
умеют с этим работать:if errors.Is(err, errRaining) {
fmt.Println("ouch!")
}
// ouch!
fmt.Errorf
тоже научилась комбинировать ошибки:err := fmt.Errorf(
"reasons to skip work: %w, %w",
errRaining,
errWindy,
)
[]error
из метода Unwrap
:type RefusalErr struct {
reasons []error
}
func (e RefusalErr) Unwrap() []error {
return e.reasons
}
func (e RefusalErr) Error() string {
return fmt.Sprintf("refusing: %v", e.reasons)
}
Атомарность и коммутативность
Во всех примерах increment()
— не атомарная операция. Композиция атомиков всегда не атомарна.
Но первый пример при этом гарантирует итоговое значение counter
в многозадачной среде:
var counter atomic.Int32
func increment() {
counter.Add(1)
sleep(10)
counter.Add(1)
}
counter
в итоге будет равен 200 (если в процессе выполнения не было ошибок).Add()
— коммутативная операция (точнее, sequence-independent, но это на русский нормально не переводится, поэтому пусть будет «коммутативная»). Такие операции можно выполнять в любом порядке, результат при этом не изменится.Пример 3 (последний)
var delta atomic.Int32Читать полностью…
var counter atomic.Int32
func increment() {
delta.Add(1)
sleep(10)
counter.Add(delta.Load())
}
Пример 1
var counter atomic.Int32Читать полностью…
func increment() {
counter.Add(1)
sleep(10)
counter.Add(1)
}