2615
Здравый взгляд на язык программирования Go. Злой админ @nalgeon. Добрый админ @mikeberezin. Рекламы нет.
Go 1.26: Множественные хендлеры в логах
Теперь можно легко писать логи сразу в несколько мест — например, в консоль, файл или на удаленный сервер — с помощью стандартной библиотеки.
Все благодаря slog.MultiHandler, который отправляет записи лога всем настроенным обработчикам.
Например, создаем хендлер для записи в stdout:
stdoutHand := slog.NewTextHandler(os.Stdout, nil)
const flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
file, err := os.OpenFile("/tmp/app.log", flags, 0644)
if err != nil { log.Fatal(err) }
defer file.Close()
fileHand := slog.NewJSONHandler(file, nil)
multiHand := slog.NewMultiHandler(stdoutHand, fileHand)
logger := slog.New(multiHand)
logger.Info("login",
slog.String("name", "whoami"),
slog.Int("id", 42),
)
Go 1.26: Байтовое подсматривание
Новый метод Buffer.Peek в пакете bytes возвращает следующие N байт буфера, не меняя при этом текущую позицию:
buf := bytes.NewBufferString("I love bytes")
sample, err := buf.Peek(1)
fmt.Println(sample)
// I
buf.Next(2)
sample, err = buf.Peek(4)
fmt.Println(sample)
// lovebuf := bytes.NewBufferString("car")
sample, err := buf.Peek(3)
fmt.Println(sample)
// car
sample[2] = 't' // меняем в буфере
data, err := buf.ReadBytes(0)
fmt.Println(sample)
// cat
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 1.26: Green Tea GC
Новый сборщик мусора (экспериментальный в версии 1.25), в 1.26 будет включен по умолчанию.
Мотивация
Старый алгоритм сборщика мусора в Go работает с графом: объекты — узлы, а указатели — связи между ними. При этом не учитывается, где именно в памяти находятся эти объекты. Сканер перескакивает между разными участками памяти, из-за чего часто происходят кэш-промахи.
В итоге процессор тратит слишком много времени, просто ожидая, когда нужные данные поступят из памяти. Больше 35% времени, потраченного на сканирование памяти, уходит впустую на ожидание доступа к данным. С увеличением числа ядер в процессоре ситуация только ухудшается.
Реализация
Green Tea меняет подход: теперь в приоритете эффективная работа с памятью. Вместо того, чтобы проверять каждый объект отдельно, система сканирует память блоками по 8 КБ (спанами). Алгоритм работает с маленькими объектами (до 512 байт), потому что их больше всего и их сложнее всего быстро просканировать.
Каждый спан делится на одинаковые слоты в зависимости от своего size-класса, и внутри него хранятся только объекты этого класса. Например, если спан относится к классу 32 байта, весь блок делится на слоты по 32 байта, и объекты кладутся прямо в эти слоты (каждый объект в начало отдельного слота). Благодаря такому фиксированному расположению сборщик мусора может легко найти метаданные объекта с помощью простой адресной арифметики, не проверяя размер каждого найденного объекта.
Когда алгоритм находит объект, который нужно просканировать, он отмечает его положение в спане, но не сканирует сразу. Вместо этого он ждет, пока в этом же спане не наберется несколько таких объектов. Затем, когда сборщик мусора обрабатывает этот спан, он сканирует сразу несколько объектов. Это намного быстрее, чем проходить по одной и той же области памяти несколько раз.
Чтобы лучше утилизировать ядра процессора, GC-воркеры делят работу между собой, «воруя» задачи друг у друга. У каждого воркера есть своя очередь задач, и если воркер простаивает, он может взять задачу из очереди другого, более занятого воркера. Такой децентрализованный подход убирает необходимость в общем списке задач, уменьшает задержки и снижает конкуренцию между ядрами процессора.
А еще Green Tea использует векторные инструкции процессора (только на архитектуре amd64), чтобы обрабатывать объекты пачками.
Бенчмарки
Результаты тестов разнятся, но команда Go ожидает, что в реальных программах, которые сильно зависят от сборщика мусора, затраты на GC снизится на 10–40%. Благодаря векторной реализации, на процессорах вроде Intel Ice Lake или AMD Zen 4 и новее, затраты на GC могут уменьшиться еще на 10%.
К сожалению, нормальных актуальных бенчмарков я не нашел 🌚
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