Одной из главных фишек языка Go является удобная работа с конкурентностью. Однако, в больших проектах всё равно возникают некоторые проблемы:
утечка горутин
некорректная обработка паник в горутинах
плохая читаемость кода
необходимость писать повторяющийся код из раза в раз
Как указывает автор библиотеки в своей статье, он часто сталкивается с ошибками при работе с горутинами, что побудило его создать новую библиотеку conc.
Особенности библиотеки
Библиотека предоставляет набор инструментов для управления конкурентностью в Go. Она позволяет синхронизировать доступ к общим ресурсам, а также контролировать выполнение горутин. Среди её особенностей можно отметить:
Свой WaitGroup без необходимости вызывать defer
Свой Pool для упрощения работы с запуска задач с ограничением параллельности выполнения
Методы для конкурентной работы со слайсами
Методы для работы с паниками в дочерних горутинах
Работа с паниками
Если вам не хочется, чтобы программа завершалась аварийно во время возникновения паники в дочерней горутине либо же вы хотите избежать других проблем, например, взаимоблокировок или утечек горутин, то очень непросто сделать это нативно с помощью стандартных библиотек:
type propagatedPanic struct {
val any
stack []byte
}
func main() {
done := make(chan *propagatedPanic)
go func() {
defer func() {
if v := recover(); v != nil {
done <- &propagatedPanic{
val: v,
stack: debug.Stack(),
}
} else {
done <- nil
}
}()
doSomethingThatMightPanic()
}()
if val := <-done; val != nil {
panic(val)
}
}
Библиотека conc справляется с поставленной задачей намного элегантнее:
func main() {
var wg conc.WaitGroup
wg.Go(doSomethingThatMightPanic)
// panics with a nice stacktrace
wg.Wait()
}
Конкурентная обработка массива данных
Зачастую необходимо обрабатывать большие объемы данных конкурентно. Для этого обычно все элементы среза отправляются в канал, откуда их забирают дочерние горутины и там же обрабатывают.
func process(values []int) {
feeder := make(chan int, 8)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for elem := range feeder {
handle(elem)
}
}()
}
for _, value := range values {
feeder <- value
}
close(feeder)
wg.Wait()
}
С библиотекой conc для этого подойдёт iter.ForEach:
func process(values []int) {
iterator := iter.Iterator[int]{
MaxGoroutines: len(input) / 2,
}
iterator.ForEach(values, handle)
}
Либо если вам нужно сопоставить элементы выходной массив так, чтобы output[i] = f(input[i])
:
func process(
input []int,
f func(int) int,
) []int {
output := make([]int, len(input))
var idx atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
i := int(idx.Add(1) - 1)
if i >= len(input) {
return
}
output[i] = f(input[i])
}
}()
}
wg.Wait()
return output
}
Гораздо проще и понятнее воспользоваться методом iter.Map:
func process(
values []int,
f func(*int) int,
) []int {
mapper := iter.Mapper[int, int]{
MaxGoroutines: len(input) / 2,
}
return mapper.Map(input, f)
}
Заключение
Выше были показаны только основные варианты работы с данной библиотекой, гораздо больше примеров вы можете найти непосредственно в исходниках. Если вам интересно, как работать с определённым методом, достаточно найти пример использования в файлах с тестами.
Также стоит отметить, что текущая версия библиотеки — pre-1.0. По заявлению разработчиков, перед выпуском версии 1.0 должны быть внесены незначительные изменения: стабилизация API и настройка параметров по умолчанию. Поэтому использовать данную библиотеку в больших проектах пока что может быть немного рискованно, но начать знакомство можно уже сейчас, тем более исходников там не слишком много (не больше 2к строк кода).
Комментарии (9)
yellow79
16.04.2023 12:49Вот благодаря таким библиотекам появляются "сеньёры", которые понятия не имеют о том, как распараллелить обработку массива штатными средствами, как обрабатывать паники, как запускать/останавливать горутины.
Dancho67
16.04.2023 12:49+2Я бы рассматривал эту библиотеку как своего рода синтаксический сахарок. Написание бойлерплейта - не делает вас более лучшим специалистом.
YekitKsv
16.04.2023 12:49+1Благодаря таким инструментам, я не сижу и копаю гигабайты логов и метрик, чтобы найти заветную горутину с которой течет память
WLMike
Бросать паники в go обычно считается антипатерном, и крутить вокруг этого целую библиотеку достаточно спорное решение
wilcot
Программа может бросать панику если есть баг в коде. Она также может это делать явно, если нарушился инвариант, который по определению не должен был нарушаться. Если из-за бага в коде возникает паника в горутине, то отсутствие recover приведет к немедленному падению всего приложения, а это не всегда хорошо. Поэтому тут есть как минимум два варианта:
Самостоятельно делать обработку паник в горутинах.
Использовать стороннюю библиотеку.
Эта библиотека как раз позволяет решить проблему, причем тут вроде как есть две опции: повторно выкинуть панику либо получить ее как ошибку.
Я конечно не сторонник таких библиотек, так как по сути дела это очень тонкая обертка над стандартными инструментами языка, но не сказать что бесполезная.
WLMike
Если вы можете продолжить работу, то это не паника, а обычная ошибка. Если есть баг в коде, то его надо править, а не обкладывать костылями. На мой взгляд, recover должен стоять в main и логировать падения, после чего systemd перезапустит программу
wilcot
Если у вас произошла паника в горутине (не в основной), то отсутствие recover приведет к падению всего приложения. Это не всегда хорошо. К примеру, у вас бекенд с несколькими маршрутами в API (/users/, /posts/ и т.д.), и так вышло, что в каком-то из обработчиков маршрутов (возможно в том, который используется крайне редко) разработчик допустил ошибку и произошел выход за пределы массива/слайса (обратите внимание, что в таком случае приложение скорее всего сможет продолжить работу). В результате, если полениться и не обработать такую панику, то пострадает вся функциональность. Если клиенсткое приложение в случае ошибки будет повторять запрос, то бекенд будет лежать и не подниматься. А еще ситуацию может усугубить например то, что бекенд будет подниматься не моментально, так как, к примеру, ему понадобится инициализировать кеши или еще что-то. В итоге из-за такого бага вы получите большой Downtime, который можно было бы избежать. Я ни в коем случае не выступаю за необходимость использования подобных библиотек (так как это очень тонкая обертка над стандартной библиотекой), но ни в коем случае не стал бы называть это антипаттерном. Особенно вариант обработки паники, с дальнейшим пробрасыванием ее в ожидающую горутину в качестве ошибки (см. WaitAndRecover).
WLMike
Для этого не нужна какая-то специальная библиотека, в большинстве библиотек для роутинга есть мидлваре, которое перехватывает панику, логирует ее и возвращает 500-ку
Ну если у вас выходы за границу слайдов случаются, то лучше ошибки икать и тесты писать, а не какие-то суррогатные библиотеки использовать для затыкания проблем
wilcot
Если в обработчике хоть где-то используется оператор go, то middleware вас не спасет.
В идеальном мире так и работает. Но речь идет про реальный мир, где люди могут совершать ошибки (и да, когда они выявляются их стараются исправить). Еще бывают баги в хардваре, или совершенно случайно флипнется бит. Да и покрыть тестами программу - дело не простое. К примеру, вы можете прочитать как достигали 100% покрытия бранчей в библиотеке sqlite: https://www.sqlite.org/testing.html
Если вам не хочется читать много текста, то вот этого будет достаточно для понимания: