2615
Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.
Go 1.26: Криптография без ридеров
Пожалуй, самое спорное изменение в Go 1.26.
Сейчас криптографические API (например ecdsa.GenerateKey), обычно принимают io.Reader как источник случайных данных:
ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
ecdsa.GenerateKey(elliptic.P256(), nil)
Go 1.26: Рекурсивные дженерики
В Go объявление дженерика содержит параметр типа и ограничение. Например:
type Foo[T any] interface {}
type Map[K comparable, V any] {}// Значение, которое можно сравнивать с другими
// значениями того же типа с помощью операции "меньше".
type Ordered[T Ordered[T]] interface {
Less(T) bool
}
// Дерево, которое хранит ordered-значения.
type Tree[T Ordered[T]] struct {
nodes []T
}
// У netip.Addr есть метод Less с нужной сигнатурой,
// поэтому он подходит для Ordered[netip.Addr].
t := Tree[netip.Addr]{}
Как выделить 33 байта
Вчера уважаемая редакция знатно ошиблась и была вынуждена отозвать свое заявление насчет make и append-make, а также компенсировать дорогим читателям проставленные лайки.
К счастью, замешательство продолжалось недолго. Сейчас разберемся.
В Go аллокатор (штука, которая выделяет память) работает с фиксированными блоками памяти (так называемые size classes) для объектов меньше 32KB. Всего таких size-класса 67 штук: 8B, 16B, 24B, 32B, 48B, ..., 28672B, 32768B.
Если вы создаете срез емкостью (capacity) 33 байта:
s := make([]byte, 0, 33)
s := append([]byte(nil), make([]byte, 33)...)[:0]
Интерактивный тур по Go 1.26
Опубликовал традиционный тур по будущему релизу (на англ). Часть фич мы с вами уже разобрали, а часть еще разберем, но если хотите прочитать все вместе уже сейчас — добро пожаловать.
Вот что вошло:
— new(expr)
— Безопасная проверка ошибок
— Новый «чайный» GC
— Ускоренный cgo и выделение памяти
— SIMD для amd64
— Секретный режим
— Криптография без ридеров
— Профиль для ловли утекающих горутин
— Метрики состояния горутин
— Итераторы в reflect
— Подсматривание в байтовый буфер
— Дескриптор процесса ОС
— Сигнал как причина в контексте
— Сравнение IP-подсетей
— Dialer с контекстом
— Фальшивый example.com
— Оптимизированные fmt.Errorf и io.ReadAll
— Множественные хендлеры в логах
— Артефакты тестов
— Обновленный go fix
Это жесть сколько всего они в релиз запихнули 😅
https://antonz.org/go-1-26
Go 1.26: Быстрое выделение памяти
В Go появились специализированные версии функции выделения памяти (allocation) для маленьких объектов (от 1 до 512 байт).
Вместо одной универсальной функции теперь выбирается конкретная реализация, в зависимости от размера объекта.
В заметках к релизу Go написано: "the compiler will now generate calls to size-specialized memory allocation routines".
Но насколько я могу судить, это не совсем так. Компилятор все еще генерирует вызов общей функции mallocgc, а та уже в рантайме перенаправляет вызов в подходящую специализированную функцию.
Для маленьких объектов это изменение уменьшает затраты на выделение памяти до 30%. Команда Go ожидает, что в реальных программах с большим количеством аллокаций общее улучшение будет около 1%.
коммит
Утечки горутин в Go 1.24+
Вы конечно и так в курсе, но на всякий случай:
Утечка происходит, если одна или несколько горутин навсегда заблокировались на канале (или другом примитиве синхронизации), но другие горутины и программа в целом при этом продолжают работать.
Утрированный пример утечки:
func work() chan int {
ch := make(chan int)
go func() {
ch <- 42
ch <- 13 // (!) утечка
}()
return ch
}
func main() {
<-work()
// ...
}func Test(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
<-work()
synctest.Wait()
})
}panic: main bubble goroutine has exited but blocked goroutines remain
goroutine 37 [chan send (durable), synctest bubble 1]:
main.work.func1()
leak_test.go:12 +0x3c
created by main.work in goroutine 36
leak_test.go:10 +0x6c
func main() {
prof := pprof.Lookup("goroutineleak")
defer func() {
time.Sleep(50 * time.Millisecond)
prof.WriteTo(os.Stdout, 2)
}()
<-work()
}goroutine 3 [chan send (leaked)]:
main.work.func1()
leak.go:12 +0x3c
created by main.work in goroutine 1
leak.go:10 +0x74
map[T]struct{}
В отличие от питона и джавы, в го нет «родного» типа «множество» (set). Поэтому разработчики наловчились использовать вместо него map[T]struct{}:
set := make(map[string]struct{})
set["apple"] = struct{}{}
if _, exists := set["apple"]; exists {
fmt.Println("✓ apple")
}✓ apple
[K1, K2... K8] [V1, V2... V8]
[ {K1, V1}, {K2, V2}, ... ]struct {
key keyType
elem elemType
}
Go 1.26: метрики горутин
Пакет runtime/metrics и раньше предоставлял вагон полезной информации о состоянии рантайма.
Но вот про горутины там было очень скромно: единственный показатель /sched/goroutines:goroutines — общее количество живых горутин.
Поэтому решили досыпать горутинных метрик:
— Общее количество горутин с начала работы программы.
— Количество горутин в каждом состоянии (ожидают, работают, ушли в системные вызовы).
— Количество активных потоков.
Такие метрики помогут быстрее замечать типичные проблемы в продакшене. Например:
→ Растет число горутин в состоянии waiting — может указывать на проблемы с блокировками.
→ Много горутин в состоянии not-in-go — они застревают в системных вызовах или cgo.
→ Увеличивается очередь runnable — CPU не справляются с нагрузкой.
Полный список новых метрик и примерчик смотрите на сайте:
https://antonz.ru/accepted/goroutine-metrics
Go 1.26: Секретный режим
В новой версии Go добавили СЕКРЕТНЫЙ пакет. Никому о нем не рассказывайте! 🤫
Ладно, ладно. Пакет действительно называется secret, но нужен он для автоматической очистки памяти, чтобы уменьшить вероятность утечки секретных ключей. Поэтому такое название.
Дело в том, что память в Go управляется рантаймом. Он не дает гарантий, когда и как именно память будет очищена. Поэтому если вы реализуете какой-то протокол шифрования, и (временно) держите в памяти секретные ключи, то совершенно непонятно, когда они из памяти удалятся.
Так что когда злодей украдет у вас дамп, то вполне может получить доступ к секретикам. Не очень хорошо.
Новый «секретный режим» в Go работает так. Запускаете свою функцию внутри secret.Do и делаете там разные грязные делишки: создаете сессионные ключи, шифры и все такое прочее. А когда функция отработает, рантайм стремительно очистит (затрет нулями) использованные регистры CPU и стековую память.
secret.Do(func() {
// Сгенерировть сессионный ключ
// и зашифровать им данные.
})
Респектабельная швейцарская карта
Некоторые интервьюеры любят расспрашивать про устройство карты в Go. Если вы в ответ на такие вопросы привыкли заводить песню о хешировании и бакетах, в которых идет линейный перебор — то пора менять репертуар :)
Карта в Go 1.24+ перешла на открытую адресацию, но с более продвинутым и эффективным методом пробирования, основанным на контрольных словах. Алгоритм использует контрольные байты и SIMD-инструкции процессора, чтобы моментально определить подходящее место для ключа и избежать длительного перебора.
За счет нового подхода удалось сделать карту более компактной и одновременно с этим улучшить быстродействие — хотя это, вообще-то, конфликтующие цели.
Была проблема в «классической» реализации swiss-таблиц — непредсказуемо большое время на рост таблицы, когда срабатывает порог по заполненности, и приходится копировать весь массив под таблицей и перехешировать в нем ключи. В Go обошли это, добавив «мета-таблицу», состоящую из множества маленьких таблиц с независимым ростом.
Такое решение, в свою очередь, породило сложности с итерацией по карте, если в процессе обхода она еще и модифицируется. Но и это тоже решили.
Если все это добро вам интересно, рекомендую короткое (всего полчаса) и очень внятное видео от Майкла Пратта, одного из авторов новой карты в Go.
Faster Go Maps With Swiss Tables
Go 1.26: Безопасная проверка ошибок
Новая функция errors.AsType — это generic-версия errors.As. Она безопасная (type-safe), работает быстрее и проще в использовании.
Сравните errors.As:
var appErr AppError
if errors.As(err, &appErr) {
fmt.Println("Got an AppError:", appErr)
}
if appErr, ok := errors.AsType[AppError](err); ok {
fmt.Println("Got an AppError:", appErr)
}
Go 1.26: Хешеры
В Go есть универсальная коллекция, основанная на хешировании — это карта (для быстрого доступа ключи карты хешируются и превращаются в индекс массива). Хеширование там под капотом, так что задумываться о нем не приходится. Обычно все так и делают.
Но иногда приходится разрабатывать карту, множество, или другую коллекцию «с нуля» — и тут от хеширования никуда не деться. В Go есть пакет maphash с полезными утилитами, но не хватало стандартного подхода к хешам в коллекциях.
Новый интерфейс maphash.Hasher — это стандартный способ хешировать и сравнивать элементы в пользовательских коллекциях:
// Hasher реализует хеширование
// и проверку на равенство для типа T.
type Hasher[T any] interface {
Hash(hash *maphash.Hash, value T)
Equal(T, T) bool
}
time.Sleep в synctest-тесте
Есть ну очень простой тест:
func Test(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var done atomic.Bool
go func() {
time.Sleep(3*time.Second)
done.Store(true)
}()
if !done.Load() {
t.Error("done=false")
}
})
}main bubble goroutine has exited but blocked goroutines remain
Тестирование многозадачности
С появлением пакета synctest в Go 1.25 тестировать многозадачный код стало приятнее, а сами тесты заметно упростились.
Но, несмотря на внешнюю безобидность (всего 2 функции!), synctest довольно непростой зверь.
Поэтому я написал по нему здоровенный урок на курсе по многозадачности. Из него вы узнаете:
— Как дождаться завершения горутины и проверить утечки.
— Что такое устойчивая блокировка и как с ней жить.
— Как использовать мгновенное ожидание.
— И вообще все о жизни внутри «пузыря» synctest.
В ближайшие пять дней урок открыт для всех, так что если хотите прокачать навыки — приглашаю пройти.
https://stepik.org/lesson/2051287
Go — №2 среди новых языков
Посмотрел всякие рейтинги распространенности языков программирования. Если брать только условно-новые языки (у кого версия 1.0 вышла после 2010 года), то топ-6 выглядит так:
➀ TypeScript
➁ Go
➂ Rust
➃ Kotlin
➄ Dart
➅ Swift
Неплохой результат!
IEEE, SO, Languish
Go 1.26: SIMD для amd64
В Go 1.26 наконец-то появились векторные операции (SIMD) в пакете simd/archsimd.
SIMD (single instruction, multiple data) — это когда одна инструкция процессора обрабатывает не одно число, а сразу несколько (вектор фиксированного размера). SIMD поддерживаются современными CPU разной архитектуры, но реализация у них отличается.
Сходу было непонятно, каким должен быть высокоуровневый кросс-платформенный API. Поэтому команда Go решила начать с низкоуровневого, и пока поддерживать только amd64 (самая популярная серверная платформа).
Думаю, это отличная идея — дать разработчикам попробовать новые возможности и собрать отзывы, прежде чем делать высокоуровневый интерфейс.
На скриншоте пример использования archsimd для сложения двух векторов произвольного размера.
Ускоренная fmt.Errorf
В Go 1.26 много улучшений производительности, но мне особенно понравилась оптимизированная fmt.Errorf — небольшое, но приятное дополнение.
Как вы знаете, errors.New("x") и fmt.Errorf("x") создают одинаковую ошибку типа errorString. Поэтому проще и единообразнее всегда использовать fmt.Errorf.
Проблема была в том, что fmt.Errorf работала в 3 раза медленнее, чем errors.New.
Один из разработчиков Go устал слышать, что "fmt.Errorf медленная", и решил это исправить. Он добавил быстрый путь, чтобы для "неформатированных" ошибок не запускалась вся тяжелая логика, а сразу вызывалась errors.New (см. скриншот).
Теперь fmt.Errorf("x") выполняет только одну аллокацию (как и errors.New) и всего на 20% медленнее (25нс против 21нс).
Очень круто!
коммит
Прожорливый make и скромный append
Читатель заметил интересный момент в новой реализации io.ReadAll. Там собирается финальный срез байт из кусочков, а перед этим для него выделяется память, вот так:
final := append([]byte(nil), make([]byte, finalSize)...)[:0]
final := make([]byte, 0, finalSize)
append([]T(nil), make([]T, n)...)
Go 1.26: Ускоренный cgo
Слоганом Go 1.26 можно делать фразу «ускорилось ВСЕ». Помимо GreenTea GC и скоростных аллокаций (о которых я писал выше) — теперь еще и cgo-вызовы работают на 20–30% быстрее.
Вот немного подробностей.
В рантайме Go используются процессоры (обычно их обозначают буквой P) — это ресурсы, которые нужны для выполнения кода. Чтобы поток мог выполнять горутину, к нему должен быть привязан конкретный процессор.
Процессоры могут находиться в разных состояниях: running (выполняет код), idle (ждет работы) или gcstop (остановлен из-за сборки мусора).
Раньше было еще состояние syscall, когда горутина делала системный или cgo-вызов. Теперь это состояние убрали.
Вместо отдельного состояния процессора рантайм просто смотрит на статус горутины, которая привязана к процессору, чтобы понять, делает ли она системный вызов.
Это уменьшает накладные расходы и упрощает работу с cgo и системными вызовами.
Красота!
Go 1.26: Обновленный go fix
С годами команда "go fix" превратилась в свалку покрытых плесенью рефакторингов, которые никто не использует.
Сейчас это изменится.
В 1.26 go fix переписана с нуля и использует тот же движок, что go vet — но другой набор анализаторов.
В отличие от go vet, исправления go fix безопасны (можно применять автоматически) и нацелены больше на модернизацию кода под новые фичи языка и стдлибы, а не исправление проблем в коде.
Пример: замена циклов на slices.Contains:
// до go fix
func find(s []int, x int) bool {
for _, v := range s {
if x == v {
return true
}
}
return false
}
// после go fix
func find(s []int, x int) bool {
return slices.Contains(s, x)
}
map[T]struct{}: новая надежда
Проводим специальный репортаж прямиком из гитхаба, где решается судьба многострадальной карты.
Итак, map[T]struct{} стала занимать столько же, сколько map[T]bool. И это не +1 байт, как может показаться.
Данные в новой карте хранятся в группах:
type group struct {
ctrl uint64
slots [8]slot
}
type slot struct {
key K
elem E
}type slot struct {
elem E
key K
}type group struct {
ctrl uint64
keys [8]K
elems [8]V
}
Тайминги для Hello world
На приложенной картинке — глубоко ненаучный бенчмарк по компиляции и выполнению примитивной hello-world программы на разных языках.
Каких-то глубоких выводов из нее не сделаешь, просто любопытно :)
Что касается Go, то компиляция в нем работает достаточно быстро даже на проектах с большим количеством зависимостей — потому что система сборки кеширует собранные зависимости (как стдлибу, так и third-party).
Кроме того, Go компилирует каждый пакет в проекте независимо, так что если изменение локальное и не сильно транзитивно влияет на другие пакеты — скомпилируется быстро.
В общем, Go в плане компиляции молодец!
Контекст с отменой, но без отмены
Go-разработчики делятся на три группы:
1. Используют контекст для отмены.
2. Используют контекст для передачи значений.
3. Используют контекст для отмены и значений.
Оказывается, последняя группа долгое время оставалась недовольной.
Дело в том, что дочерний контекст отменяется при отмене родительского:
ctx := context.WithValue(context.Background(), "id", 42)
parentCtx, cancel := context.WithCancel(ctx)
childCtx := parentCtx
cancel()
fmt.Printf("child canceled = %v\n", childCtx.Err() != nil)
fmt.Printf("child id = %v\n", childCtx.Value("id"))
child canceled = true
child id = 42
ctx := context.WithValue(context.Background(), "id", 42)
parentCtx, cancel := context.WithCancel(ctx)
childCtx := context.Background()
cancel()
fmt.Printf("child canceled = %v\n", childCtx.Err() != nil)
fmt.Printf("child id = %v\n", childCtx.Value("id"))
child canceled = false
child id = <nil>
ctx := context.WithValue(context.Background(), "id", 42)
parentCtx, cancel := context.WithCancel(ctx)
childCtx := context.WithoutCancel(parentCtx)
cancel()
fmt.Printf("child canceled = %v\n", childCtx.Err() != nil)
fmt.Printf("child id = %v\n", childCtx.Value("id"))
child canceled = false
child id = 42
Go 1.26: Dialer с контекстом
Пакет net помимо прочего отвечает за «дозвон» (dial) к сетевым адресам по разным протоколам — TCP, UDP, IP или Unix-сокеты.
В пакете есть отдельные функции для разных протоколов: DialTCP, DialUDP, DialIP и DialUnix. Но вот беда: их сделали до появления интерфейса Context, поэтому они не поддерживают отмену.
С другой стороны, есть тип-звонилка net.Dialer, а у него универсальный метод DialContext. Он поддерживает отмену и может использоваться для подключения по любому из поддерживаемых протоколов. Но вот беда: он более тормозной, чем отдельные функции (потому что универсальный).
В итоге получилось и то не то, и это не это.
Поэтому решили докинуть типу Dialer новых методов — чтобы и быстрые были, и отмену поддерживали. Теперь будут и функции net.DialШто и методы net.Dialer.DialШто.
Не перепутайте!
Шучу, вы их все равно использовать не будете 😁
https://antonz.ru/accepted/net-dialer-context
Go 1.26: Сравнение IP-подсетей
Префикс IP-адреса задает определенную подсеть. Обычно такие префиксы записывают в формате CIDR:
10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
203.0.113.0/24
prefixes := []netip.Prefix{
netip.MustParsePrefix("10.1.0.0/16"),
netip.MustParsePrefix("203.0.113.0/24"),
netip.MustParsePrefix("10.0.0.0/16"),
netip.MustParsePrefix("169.254.0.0/16"),
netip.MustParsePrefix("203.0.113.0/8"),
}
slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
return a.Compare(b)
})10.0.0.0/16
10.1.0.0/16
169.254.0.0/16
203.0.113.0/8
203.0.113.0/24
Знакомство с Go
Я опубликовал бесплатный вводный курс по Go. Он идеально подходит для свитчеров — тех, кто уже уверенно программирует на другом языке и хочет попробовать Go.
В отличие от обычных курсов-знакомств, тут никто не рассказывает, что такое переменная и чем она отличается от цикла. Бестолковых задач вроде «что напечатает функция» тоже нет. Все кратко и по делу.
Освоить язык целиком по курсу не получится (для этого у меня есть другой курс, я же матерый инфоцыган препод), а вот понять «это вообще мое или нет» — запросто.
Так что если захотите сманить в го знакомого похапэра или питоняшу — можно смело рекомендовать :)
https://stepik.org/a/263524
P.S. Если вы записаны на курс по многозадачности — там на уроке про внутрянку добавилась теория про метрики, профилирование и трассировку. И задачка!
Go 1.26: new(expr)
Наступила осень, а значит пора разбирать новые фичи, запланированные в Go 1.26 (но это вам на новый год!)
Начнем со встроенной функции (builtin) new, которую теперь можно использовать для любых выражений (кроме nil).
Раньше new можно было использовать только с типами:
p1 := new(int)
fmt.Println(*p1)
// 0
// Указатель на переменную типа int со значением 42.
p := new(42)
fmt.Println(*p)
// 42
f := func() string { return "go" }
p := new(f())
fmt.Println(*p)
// go
time.Sleep в тесте
Есть ну очень простой тест:
func Test(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var done atomic.Bool
go func() {
time.Sleep(3*time.Second)
done.Store(true)
}()
if !done.Load() {
t.Errorf("done=false")
}
})
}go test с параметром timeout=1s? Чур без спойлеров в комментариях.
Факты о дорогих читателях
В сентябре мы приглашали заполнить ежегодный опросник по Go, а теперь подоспели результаты.
Некоторые факты от которых задергался глаз у редакции:
Джунов на Go только 10%. Начал писать на го — считай сразу сеньор.
Половина го-разработчиков изменяют с питоном.
94% разработчиков в офисе никто не ждет.
Если вы не разработали на Go хотя бы одну апишечку, то жизни не знаете.
Две трети разработчиков несчастливы одинаково: они используют кубернетис.
Половина разработчиков используют контрабандный GoLand.
3% работают в подвале без интернета, поэтому у них Go версии 1.20.
11 человек получают 500 тыщ рублей ежемесячно за свои труды.
Есть хорошие шансы, что вы знаете, кто такой Николай Тузов (я вот понятия не имею).
Полный отчет: https://devcrowd.ru/go-2025/
Горутины и потоки
Go не дает программисту явно управлять потоками.
Предполагается, что мы просто создаем горутины, а рантайм сам разберется, как и на каких потоках операционной системы их исполнять.
Но если очень хочется, можно прибить горутину к конкретному потоку через runtime.LockOSThread:
var wg sync.WaitGroup
wg.Add(nWorkers)
for range nWorkers {
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer wg.Done()
// do work
}()
}
wg.Wait()