Честно: я тоже на это наступал. Причём не один раз.
В PHP всё просто — клиент отвалился, процесс умер, соединения закрылись. В Go так не работает.
context.Context — это не "таймаут на запрос", это контракт: "вот сигнал остановиться, будь добр, услышь его". Если его игнорировать, работа продолжается, хотя результат уже никому не нужен. А ресурсы — коннекты к БД, горутины, память — висят.Классика, которую я ловил:
- Горутина, запущенная из хендлера, не слушает
ctx.Done().-
http.Client без таймаута и без контекста в запросе.-
db.Query(...) вместо db.QueryContext(ctx, ...).Вот так горутина и утекает:
// было: горутина живёт дольше, чем нужно
func handler(w http.ResponseWriter, r *http.Request) {
go func() {
for {
doWork() // никто не останавливает
time.Sleep(time.Second)
}
}()
}
// стало: горутина слышит отмену и выходит
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
time.Sleep(time.Second)
}
}
}()
}
Без
select с <-ctx.Done() горутина переживёт и хендлер, и смысл своего существования.Самое противное — оно не падает. Просто под нагрузкой медленно течёт: пул коннектов забивается, горутин всё больше, память ползёт вверх. Ищется потом долго.
Мораль: в Go отмену надо прокидывать руками. Везде. Привычка из PHP "оно само" тут не работает.