Одной из главных фишек языка 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)


  1. WLMike
    16.04.2023 12:49
    +4

    Бросать паники в go обычно считается антипатерном, и крутить вокруг этого целую библиотеку достаточно спорное решение


    1. wilcot
      16.04.2023 12:49

      Программа может бросать панику если есть баг в коде. Она также может это делать явно, если нарушился инвариант, который по определению не должен был нарушаться. Если из-за бага в коде возникает паника в горутине, то отсутствие recover приведет к немедленному падению всего приложения, а это не всегда хорошо. Поэтому тут есть как минимум два варианта:

      1. Самостоятельно делать обработку паник в горутинах.

      2. Использовать стороннюю библиотеку.

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

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


      1. WLMike
        16.04.2023 12:49
        +2

        Если вы можете продолжить работу, то это не паника, а обычная ошибка. Если есть баг в коде, то его надо править, а не обкладывать костылями. На мой взгляд, recover должен стоять в main и логировать падения, после чего systemd перезапустит программу


        1. wilcot
          16.04.2023 12:49

          Если у вас произошла паника в горутине (не в основной), то отсутствие recover приведет к падению всего приложения. Это не всегда хорошо. К примеру, у вас бекенд с несколькими маршрутами в API (/users/, /posts/ и т.д.), и так вышло, что в каком-то из обработчиков маршрутов (возможно в том, который используется крайне редко) разработчик допустил ошибку и произошел выход за пределы массива/слайса (обратите внимание, что в таком случае приложение скорее всего сможет продолжить работу). В результате, если полениться и не обработать такую панику, то пострадает вся функциональность. Если клиенсткое приложение в случае ошибки будет повторять запрос, то бекенд будет лежать и не подниматься. А еще ситуацию может усугубить например то, что бекенд будет подниматься не моментально, так как, к примеру, ему понадобится инициализировать кеши или еще что-то. В итоге из-за такого бага вы получите большой Downtime, который можно было бы избежать. Я ни в коем случае не выступаю за необходимость использования подобных библиотек (так как это очень тонкая обертка над стандартной библиотекой), но ни в коем случае не стал бы называть это антипаттерном. Особенно вариант обработки паники, с дальнейшим пробрасыванием ее в ожидающую горутину в качестве ошибки (см. WaitAndRecover).


          1. WLMike
            16.04.2023 12:49
            +2

            Для этого не нужна какая-то специальная библиотека, в большинстве библиотек для роутинга есть мидлваре, которое перехватывает панику, логирует ее и возвращает 500-ку

            Ну если у вас выходы за границу слайдов случаются, то лучше ошибки икать и тесты писать, а не какие-то суррогатные библиотеки использовать для затыкания проблем


            1. wilcot
              16.04.2023 12:49

              Для этого не нужна какая-то специальная библиотека, в большинстве библиотек для роутинга есть мидлваре, которое перехватывает панику, логирует ее и возвращает 500-ку

              Если в обработчике хоть где-то используется оператор go, то middleware вас не спасет.

              Ну если у вас выходы за границу слайдов случаются, то лучше ошибки икать и тесты писать, а не какие-то суррогатные библиотеки использовать для затыкания проблем

              В идеальном мире так и работает. Но речь идет про реальный мир, где люди могут совершать ошибки (и да, когда они выявляются их стараются исправить). Еще бывают баги в хардваре, или совершенно случайно флипнется бит. Да и покрыть тестами программу - дело не простое. К примеру, вы можете прочитать как достигали 100% покрытия бранчей в библиотеке sqlite: https://www.sqlite.org/testing.html

              Если вам не хочется читать много текста, то вот этого будет достаточно для понимания:

              As of version 3.39.0 (2022-06-25), the SQLite library consists of approximately 151.3 KSLOC of C code. (KSLOC means thousands of "Source Lines Of Code" or, in other words, lines of code excluding blank lines and comments.) By comparison, the project has 608 times as much test code and test scripts - 92038.3 KSLOC.


  1. yellow79
    16.04.2023 12:49

    Вот благодаря таким библиотекам появляются "сеньёры", которые понятия не имеют о том, как распараллелить обработку массива штатными средствами, как обрабатывать паники, как запускать/останавливать горутины.


    1. Dancho67
      16.04.2023 12:49
      +2

      Я бы рассматривал эту библиотеку как своего рода синтаксический сахарок. Написание бойлерплейта - не делает вас более лучшим специалистом.


    1. YekitKsv
      16.04.2023 12:49
      +1

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