Go: почему time.After() в цикле может стать проблемой ⏱️
В go есть удобный способ подождать некоторое время:
select { case <-time.After(time.Second): // Продолжаем работу }
Но если использовать time.After() в цикле, особенно с долгим таймаутом, можно получить неприятный сюрприз.
for { select { case msg := <-messages: handle(msg) case <-time.After(30 * time.Second): log.Println("timeout") } }
Что здесь не так? Каждый раз, когда цикл доходит до select, создаётся новый таймер. Если приходит сообщение, ветка с таймером не срабатывает, но сам таймер продолжает существовать до истечения срока. При большом количестве сообщений и длинном таймауте такие таймеры могут накапливаться и создавать лишнюю нагрузку на память и сборщик мусора. Как лучше?
Если нужен один таймаут на ожидание, создайте таймер заранее:
timer := time.NewTimer(30 * time.Second) defer timer.Stop() for { select { case msg := <-messages: handle(msg) case <-timer.C: log.Println("timeout") return } }
Теперь используется один таймер, а не новый на каждой итерации.
Но есть важный нюанс: в этом варианте таймер сработает через 30 секунд от момента создания, даже если сообщения продолжают поступать. Если нужен таймаут бездействия, таймер нужно сбрасывать после получения сообщения, корректно обрабатывая его состояние.
Вывод: time.After() удобен для разовых ожиданий, но в горячих циклах стоит внимательно следить за созданием таймеров.