Golang Portal
8.13K subscribers
429 photos
27 videos
7 files
458 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для Golang-разработчика

Связь: @devmangx
Download Telegram
Оптимизируйте множественные вызовы с помощью singleflight

Допустим, у вас есть функция, которая получает данные по сети или выполняет ввод-вывод, и её выполнение занимает около 3 секунд:

Эта функция выдаёт новое значение каждые 10 секунд.

🔹Если вызвать эту функцию 3 раза подряд, общее время ожидания составит примерно 9 секунд.
🔹Если использовать 3 горутины, общее время ожидания может сократиться до 3 секунд, но функция всё равно будет вызвана 3 раза для получения одного и того же результата (~99%).

Здесь на помощь приходит пакет singleflight, который доступен по ссылке:
👉 golang.org/x/sync/singleflight.

Этот пакет позволяет выполнить функцию только один раз, независимо от того, сколько раз она была вызвана за эти 3 секунды, и возвращает один общий результат.

Это отлично подходит для оптимизации функций, работающих долго или потребляющих много ресурсов.

Как это работает?
1️⃣ Создаём объект singleflight.Group
2️⃣ Передаём в метод group.Do() функцию, которая выполняет дорогостоящие вычисления.

Метод group.Do() возвращает:
(result any, err error, shared bool)

Параметр shared показывает, был ли результат разделён между несколькими вызовами.

Зачем нужен аргумент key?
Ключ (key) — это идентификатор запроса.
Если поступает несколько запросов с одним и тем же ключом, singleflight понимает, что они запрашивают одно и то же.

Пример работы: Go Playground

С этим подходом, если функция вызывается несколько раз одновременно, фактически выполняется только один её вызов. А результат этого единственного вызова разделяется между всеми ожидающими

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍102
Фильтрация без лишних аллокаций

При фильтрации слайсов в Go стандартный подход — создание нового слайса для отфильтрованных элементов.

Однако такой метод приводит к дополнительным аллокациям памяти.

Более эффективный способ — фильтровать «на месте», используя исходный массив слайса.

Как это работает:
filtered := numbers[:0] создаёт новый слайс filtered, который ссылается на тот же массив, что и numbers, но имеет нулевую длину, сохраняя емкость numbers.
• Добавляя num в filtered, мы избегаем лишних аллокаций, так как просто изменяем содержимое numbers (или его базового массива).

Таким образом, мы не выделяем новую память, а заполняем уже существующий массив.

Этот метод особенно полезен, когда:
numbers больше не нужен после фильтрации.
• Критична производительность, особенно при работе с большими объемами данных.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🔥73👎1
Продолжайте работу с контекстами с помощью context.WithoutCancel()

Мы уже знаем, что при отмене родительского контекста отменяются и все его дочерние контексты, верно?
Но иногда это поведение нежелательно.

Есть случаи, когда нужно, чтобы определённые операции продолжались, даже если родительский контекст был отменён.

Представьте, что вы обрабатываете HTTP-запрос, и при его отмене (например, из-за таймаута или разрыва соединения с клиентом) всё ещё необходимо залогировать информацию о запросе и собрать метрики.

«Ха, просто создам новый контекст для этих операций»


Это возможно, но новый контекст не содержит значений из исходного контекста события, которые важны, например, для логирования или сбора метрик.

Передача значений возможна только через дочерний контекст.
Возвращаясь к примеру с HTTP-запросом, решение следующее:

context.WithoutCancel позволяет создать дочерний контекст, который не будет отменён при отмене родительского. Это обеспечивает завершение нужных операций даже после отмены запроса.

Кстати, эта функция появилась в Go 1.21. ✌️

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍113
Избегайте использования math/rand, вместо этого используйте crypto/rand для генерации ключей

Когда вы работаете над проектами, где нужно генерировать ключи — например, для шифрования или создания уникальных идентификаторов — качество и безопасность этих ключей имеют большое значение.

Почему не math/rand?

Пакет math/rand генерирует псевдослучайные числа.

Это означает, что если известен способ генерации чисел (seed), можно предсказать их значения.
Даже если использовать в качестве seed текущее время (например, time.Now().UnixNano()), уровень непредсказуемости (энтропии) остаётся низким, потому что время между запусками не сильно отличается.

Почему crypto/rand?

Пакет crypto/rand предоставляет способ генерации криптографически стойких случайных чисел.

Он спроектирован таким образом, чтобы быть непредсказуемым, используя источники случайности от операционной системы, которые гораздо сложнее предугадать.
crypto/rand подходит для шифрования, аутентификации и других операций, чувствительных к безопасности.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍202
Получаем указатели проще с помощью дженериков

Небольшой совет для тех, кто пишет на Go и часто сталкивается с необходимостью получить указатель на значение.

Раньше вы, возможно, делали так (1 картинка)

Или пытались уместить всё в одну строчку, используя небольшой трюк (2 картинка)

Такой способ рабочий, но выглядит громоздко, особенно если приходится делать это часто для разных типов данных.

Теперь давайте посмотрим на более простой и современный способ с дженериками (3 картинка)

Эта небольшая функция позволяет создавать указатель для любого типа значения, не повторяя один и тот же код снова и снова.

Просто передайте своё значение в функцию Ptr, и вы получите нужный указатель (4 картинка)

Такой подход делает код чище и избавляет от лишнего дублирования.

Больше не нужно писать отдельные функции или вручную обрабатывать указатели для каждого типа. Держите код простым и сосредоточьтесь на главном.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
10👍6👎4🤔2
Писать неудобный код — сложно, но приходится

Когда я делаю ревью, я всегда убеждаю писать такой код, который может быть неудобно реализовывать, но который удобно использовать и сложно неправильно применить.

Например, чтобы обновить документ в базе данных — почему бы просто не передать клиенту map[string]any, чтобы он сам обновил, что ему нужно?
Лично я такое не приемлю, особенно в командной работе.

Я бы ограничил, какие поля может обновлять клиент, и валидировал бы всё перед записью в базу, даже если для этого нужно больше кода, больше функций.

Я не хочу давать клиенту лишние шансы ошибиться.

Использование any, map[string]interface{}, возврат пустых error, глобальные переменные, сваливание всех хелперов в shared/utils, создание монолитных общих интерфейсов, игнорирование ошибок (или возвращаемых значений) без комментариев, написание огромных функций без разбиения, использование context.Background() ради скорости, возврат 3–4 значений, хардкод вместо констант, наплевательское отношение к дизайну и зависимостям между пакетами, сваливание всех моделей в models/model.go

Я не утверждаю, что всё это всегда плохо — как обычно, всё зависит от контекста.

Опытные разработчики знают, когда можно отойти от правил, но никогда не злоупотребляют этим.

Каждая из этих практик сама по себе может быть не критична, но в сумме они могут привести к серьёзным проблемам в будущем.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
👍147🔥3
Mutex — это концепт, который часто вызывает путаницу, хотя на самом деле он довольно прост для понимания. Mutex — это примитив синхронизации, который помогает избежать гонки данных (race conditions).

Предположим, вы обрабатываете директорию с файлами и хотите сохранить результат обработки каждого файла в срез. Чтобы избежать ситуации, когда несколько горутин одновременно пытаются записать данные в срез, необходимо синхронизировать доступ к нему.

Mutex (сокращение от mutual exclusion) предоставляет две основные функции — Lock и Unlock, а также внутренний счётчик.

🔹counter = 0 означает, что mutex разблокирован

🔹counter = 1 означает, что mutex заблокирован

Когда первая горутина вызывает Lock, она блокирует доступ к срезу, установив счётчик в 1. Следующая горутина, попытавшаяся войти, будет заблокирована (впадёт в ожидание), пока счётчик не станет равным 0 — то есть пока первая горутина не вызовет Unlock.

По сути, mutex обеспечивает, чтобы только один поток выполнения (в данном случае — горутина) имел доступ к общему ресурсу в определённый момент времени. Это особенно полезно, когда, например, нужно избежать ситуации, при которой две горутины одновременно записывают данные в одну и ту же позицию среза.

Семафор — это расширение концепции mutex. Если mutex допускает только одного "оператора", то семафор может иметь произвольное количество слотов. Например, sync.WaitGroup в Go использует семафорную модель под капотом.

На картинке приведён пример кастомной реализации mutex

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
👍97
До сих пор самым впечатляющим аспектом Go для меня остаются его примитивы конкурентности.

В традиционных языках программирования конкурентность достигается за счёт потоков операционной системы. Например, если у вас CPU с 6 ядрами и поддержкой 2 логических потоков на ядро, вы можете распараллелить выполнение максимум на 12 потоков.

В Go есть собственный планировщик задач, встроенный в рантайм. Он не зависит от потоков ОС для запуска задач.

Этот лёгкий, пользовательский планировщик работает в пространстве пользователя и позволяет запускать тысячи или даже миллионы конкурентных путей исполнения без накладных расходов, связанных с потоками ОС.

Когда вы запускаете горутину с помощью ключевого слова go перед вызовом функции, рантайм Go выделяет небольшой участок памяти под стек этой горутины.

Вызов функции и весь его контекст размещаются на этом стеке, создавая независимый путь исполнения, который может выполняться параллельно с другими горутинами.

Синхронизировать горутины можно через sync.WaitGroup или использовать каналы (chan) для обмена данными и координации между ними.

Именно такая лёгкая модель выделения памяти и позволяет Go эффективно управлять тысячами и миллионами конкурентных задач.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥2👍1
Каналы в Go — это интересная и поначалу достаточно сложная для понимания концепция.

Если вы пришли из традиционных языков, таких как Java, то вы, скорее всего, использовали потоки с мьютексами или асинхронные очереди вроде Apache Kafka для организации взаимодействия между разными частями программы.

Синтаксис каналов может показаться странным, а семантика — расплывчатой, но они становятся понятнее, если воспринимать их как конкурентные очереди.

Канал в Go — это просто очередь, через которую можно передавать данные.

Если вы записываете в канал — это эквивалентно добавлению элемента в очередь.
Если вы читаете из канала — вы извлекаете первый элемент из очереди.
Очередь работает по принципу FIFO (первым пришёл — первым вышел).

У каналов множество применений. Например, представьте, что у вас есть HTTP-обработчик, и после завершения его работы вы хотите запустить дополнительную вычислительную задачу. Вы могли бы использовать асинхронную очередь для запуска workflow, а можете — канал.

В коде вы просто записываете значение в канал, например struct{user userID; data interface{}}, и это может послужить триггером для запуска другой вычислительной задачи.

Каналы безопасны для конкурентного чтения и записи — для записи в канал не нужен мьютекс.

Вы можете контролировать, сколько сигналов канал может буферизовать, указывая размер буфера при создании через make (например, userChan := make(chan interface{}, capacity)).

Можно создать канал, принимающий единичный сигнал (например, сигнал для закрытия базы данных), или буферизированный канал, в который несколько горутин записывают результаты своей работы.

Если количество горутин, пытающихся записать в канал, превышает его ёмкость, то лишние горутины будут блокироваться (то есть ждать), пока данные не будут считаны и не освободится место.

Из-за такого блокирующего поведения каналы иногда используют в качестве семафоров (хотя для этого существуют специализированные пакеты, которые лучше использовать вместо каналов).

По сути, каналы — это отличный способ организации взаимодействия между последовательными процессами, что и составляет суть модели конкурентности CSP (Communicating Sequential Processes) в Go.

Вот пример кода, показывающий мощь каналов. В Go существует возможность подписаться на сигналы операционной системы через канал.

Например, если программа завершается, вызывает панику, прерывается или останавливается — мы можем получить сообщение об этом через канал и корректно завершить работу сервера (записать состояние в лог, закрыть подключения к БД, завершить файловые операции и т. д.).

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
6👍4
Начиная с Go 1.8, стандартная библиотека http.Server включает метод Shutdown, который критически важен для корректного завершения работы веб-сервисов.

Этот метод обеспечивает "плавное" завершение работы сервера, поскольку он:

🔹Немедленно прекращает приём новых подключений

🔹Сохраняет существующие подключения активными до завершения всех текущих запросов

🔹Предоставляет контекст для управления таймаутом на завершение "висящих" запросов

🔹Завершается только после обработки всех находящихся в процессе выполнения запросов

🔹Предотвращает утечки подключений при завершении работы приложения

Без корректного завершения, принудительное завершение работы сервера может привести к обрыву активных запросов, ошибкам на стороне клиента и потенциальной неконсистентности данных.

Метод Shutdown элегантно решает эту проблему. Ниже приведена полноценная реализация, демонстрирующая обработку нескольких сигналов ОС для плавного завершения работы:

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
14👍9
При разработке приложений, использующих LLM API, таких как OpenAI или Anthropic, вы быстро упрётесь в лимиты запросов при масштабной обработке. Пакет rate из https://golang.org — хорошее решение этой проблемы, так как он:

🔹Реализует лимитирование скорости на основе алгоритма token bucket с точной настройкой

🔹Поддерживает ожидание с учётом контекста и возможностью отмены

🔹Эффективно обрабатывает всплески нагрузки благодаря настраиваемому размеру «ведра»

🔹Органично интегрируется с моделью конкурентности Go

Алгоритм token bucket особенно хорошо подходит для LLM‑API. Каждый запрос потребляет один токен из «бакета», который пополняется с заданной скоростью. Если «бакет» пуст, запросы ожидают появления токенов вместо того, чтобы сразу завершаться с ошибкой.

Выше приведён пример кода с неофициальным клиентом OpenAI и использованием пакета rate

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
👍71
Если вы обрабатываете HTTP-запросы на Go, сжатие gzip является важным инструментом для снижения потребления трафика и ускорения отклика. Пакет encoding/gzip упрощает реализацию, но есть некоторые подводные камни, о которых стоит помнить.

При реализации gzip-сжатия в веб-серверах:

🔹Всегда проверяйте заголовок Accept-Encoding перед применением сжатия

🔹Устанавливайте корректный заголовок ответа Content-Encoding

🔹Избегайте сжатия уже сжатых форматов (например, изображений, видео)

🔹Используйте sync.Pool для повторного использования gzip-писателей и повышения производительности

Вот пример реализации с использованием паттерна middleware.

👉 @juniorGolang | #tip
Please open Telegram to view this post
VIEW IN TELEGRAM
👍142
Начиная с Go 1.26 можно передавать выражение в new(), чтобы напрямую получить указатель на результат этого выражения.

До Go 1.26 можно было делать только new(Type), чтобы получить указатель на нулевое значение этого типа.

Новое поведение new(expr) — это синтаксический сахар для:
tmp := expr
result := &tmp


Но есть 3 нюанса:

1. Если expr уже является указателем, то new(expr) вернёт указатель на этот указатель.
new(bytes.NewBuffer(nil)) → **bytes.Buffer


2. new(expr) копирует значение expr во вновь созданную переменную.
i := 1
p := new(i)
i = 2 // → *p всё ещё равно 1


3. Нетипизированные константы сначала получают тип по умолчанию:
new(123)  → *int
new(1.2) → *float64
new('a') → *rune

new(nil) — невалидно.

👉 @GolangPortal | #tip by Phuong Le
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥2🤔1
Многие статьи заставляют думать, что go func() {} всегда создаёт горутину с 2 KiB стека. Иногда так и есть, но далеко не всегда.

Go держит пул переиспользуемых горутин на каждый процессор и глобальный пул, причём у части горутин в этих пулах уже есть прикреплённые стеки, а у части нет (см. диаграмму ниже)

Рантайм поддерживает динамический “начальный размер стека”, который пересчитывается в каждом цикле сборки мусора исходя из среднего использования стека всеми просканированными горутинами

Когда вы вызываете go f(), рантайм может взять горутину и стек из пулов, поэтому многие горутины на самом деле стартуют с размером стека больше фиксированного минимума 2 KiB – например, 4 KiB, 8 KiB, 16 KiB и так далее.

Если размер стека повторно используемой горутины отличается от текущего “начального размера стека”, её старый стек освобождается, и ей выделяется новый стек ровно текущего начального размера.

Если подходящей повторно используемой горутины нет, рантайм выделяет новую с фиксированным начальным стеком 2 KiB ( на большинстве 64-битных Unix-подобных платформ)

👉 @GolangPortal #tip by Phuong Le
Please open Telegram to view this post
VIEW IN TELEGRAM
13👍9👎1
Разработчиков на Go можно условно разделить на три группы

1. Тех, кто использует context для отмены.

2. Тех, кто использует context для прокидывания значений.

3. Тех, кто использует context и для отмены, и для передачи значений.

Оказалось, что последняя группа была недовольна, потому что иногда нужно отменить родительский контекст, не затрагивая дочерний, но при этом сохранить в дочернем все значения из родительского контекста.

Чтобы поддержать этот чудесный сценарий, в Go 1.21 добавили функцию context.WithoutCancel. Она отвязывает дочерний контекст от родительского в части отмены, но сохраняет все значения.

Context в Go начинался как простая, уродливая концепция. Со временем он превратился в сложную, уродливую

👉 @GolangPortal #tip by Anton Zhiyanov
Please open Telegram to view this post
VIEW IN TELEGRAM
14👍10😁9