У меня есть следующие два метода:
func DeleteSessionId(id string, ctx *context.Context) (err error) {
sqlStatement := "DELETE FROM sessions WHERE id = $1"
queryCtx, cancel := context.WithTimeout(*ctx, 60*time.Second)
defer cancel()
_, err = Constants.Db.ExecContext(queryCtx, sqlStatement, id)
if err != nil {
log.Println("An error occured while deleting session from sessions table: " + err.Error())
return err
}
return nil
}
func DeleteAllSpecificUserSessionIds(userId string, ctx context.Context) (err error) {
sqlStatement := "SELECT id from sessions where userid = $1"
queryCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
sqlRows, err := Constants.Db.QueryContext(queryCtx, sqlStatement, userId)
cancel()
if err != nil {
log.Println("An error occurred when selecting sessions from DB: " + err.Error())
return err
}
var wg sync.WaitGroup
for sqlRows.Next() {
wg.Add(1)
var sessionId string
if err := sqlRows.Scan(&sessionId); err != nil {
log.Println("An error occurred when looping through sessions from DB: " + err.Error())
return err
}
var key = CacheKeys.SessionIdKey{
SessionId: sessionId,
}
go func() {
Constants.Cache.Remove(key)
DeleteSessionId(sessionId, &ctx)
wg.Done()
}()
}
wg.Wait()
if err := sqlRows.Err(); err != nil {
log.Println("An error occurred when selecting sessions from DB: " + err.Error())
return err
}
sqlRows.Close()
return nil
}
Когда я вызываю cancel() вместо использования defer.Cancel(), метод DeleteSessionId() успешно выполняется только один раз, а последующие вызовы завершаются с ошибкой context cancelled. Таким образом, если в базе данных имеется 5 идентификаторов сеансов, удаляется только один.
Однако когда я использую defer.Cancel(), метод DeleteSessionId() успешно выполняется на всех итерациях. Итак, если в базе данных есть 5 идентификаторов сеансов, все они удаляются:
func DeleteSessionId(id string, ctx *context.Context) (err error) {
sqlStatement := "DELETE FROM sessions WHERE id = $1"
queryCtx, cancel := context.WithTimeout(*ctx, 60*time.Second)
defer cancel()
_, err = Constants.Db.ExecContext(queryCtx, sqlStatement, id)
if err != nil {
log.Println("An error occured while deleting session from sessions table: " + err.Error())
return err
}
return nil
}
func DeleteAllSpecificUserSessionIds(userId string, ctx context.Context) (err error) {
sqlStatement := "SELECT id from sessions where userid = $1"
queryCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
sqlRows, err := Constants.Db.QueryContext(queryCtx, sqlStatement, userId)
defer cancel()
if err != nil {
log.Println("An error occurred when selecting sessions from DB: " + err.Error())
return err
}
var wg sync.WaitGroup
for sqlRows.Next() {
wg.Add(1)
var sessionId string
if err := sqlRows.Scan(&sessionId); err != nil {
log.Println("An error occurred when looping through sessions from DB: " + err.Error())
return err
}
var key = CacheKeys.SessionIdKey{
SessionId: sessionId,
}
go func() {
Constants.Cache.Remove(key)
DeleteSessionId(sessionId, &ctx)
wg.Done()
}()
}
wg.Wait()
if err := sqlRows.Err(); err != nil {
log.Println("An error occurred when selecting sessions from DB: " + err.Error())
return err
}
sqlRows.Close()
return nil
}
Когда я просмотрел документацию по методу context.WithTimeout(), в документации ничего не упоминается о создаваемой копии родительского контекста. Я предположил, что context.Context, возвращаемый context.WithTimeout(), по-прежнему относится к родительскому контексту. Если это предположение верно, то вызов cancel() в позиции 1 должен привести к сбою всех моих вызовов метода DeleteSessionId(). Однако этого не происходит. Как я уже упоминал, метод DeleteSessionId() успешно запускается один раз и удаляет одну строку из моей таблицы, а затем последующие вызовы завершаются неудачно. Я хотел бы знать, почему это так.

В документе WithTimeout говорится:
WithTimeout возвращает WithDeadline(parent, time.Now().Add(timeout)).
В документе WithDeadline говорится (выделено мной):
WithDeadline возвращает копию родительского контекста с крайним сроком, установленным не позднее d. Если крайний срок родительского объекта уже раньше, чем d, WithDeadline(parent, d) семантически эквивалентен родительскому. Возвращенный канал [Context.Done] закрывается по истечении крайнего срока, при вызове возвращаемой функции отмены или при закрытии канала Done родительского контекста, в зависимости от того, что произойдет раньше.
Это означает, что WithTimeout возвращает новый контекст. Согласно документации пакета (опять же, акцент мой):
Функции WithCancel, WithDeadline и WithTimeout принимают контекст (родительский) и возвращают производный контекст (дочерний) и CancelFunc. Вызов CancelFunc отменяет дочерний элемент и его дочерние элементы, удаляет ссылку родителя на дочерний элемент и останавливает все связанные таймеры.
Таким образом, отмена контекста отменяет все дочерние элементы (контексты, производные от него), но не его родительский элемент (контекст, из которого он был создан).
Это верно. Просто хочу добавить, что это также означает, что если родительский контекст будет отменен, новый производный контекст будет отменен. Метод WithTimeout возвращает новый контекст, но он по-прежнему зависит от родительского контекста. Я указываю на это только для того, чтобы читатели поняли: когда мы говорим «новый контекст», это не означает совершенно новый контекст, который вы получаете от context.TODO().
Я нашел решение своей проблемы. Моя проблема заключалась в моей интерпретации того, как работает QueryContext. Согласно этой проблеме на github, поднятой здесь, не весь набор результатов из базы данных получен до тех пор, пока вы не завершите итерацию по набору sqlrows. Поэтому, если вы вызовете cancel сразу после инициализации sqlRows, когда вы перейдете к сканированию строки в цикле for, вы получите ошибку context cancelled.
Такое поведение не упоминается в документации для запросов к базе данных с несколькими строками, что наводит меня на мысль, что моя проблема связана с моим пониманием метода context.WithTimeout(). Фактически формулировка документации подразумевает, что после вызова QueryContext возвращается весь набор данных.
Кажется, люди просто отвергают этот вопрос, хотя не смогли помочь с поиском решения. Надеемся, что документация по методу QueryContext будет обновлена, чтобы сделать это более понятным для будущих читателей.
Часто невыразимой особенностью таких запросов к базе данных является то, что операция запроса обычно возвращает курсор, а сам курсор фактически запускает запрос и извлекает результаты в пакетном режиме. Поэтому транзакции БД должны оставаться открытыми до тех пор, пока не будут получены все результаты.
Несвязано: нет причин передавать указатель на контекст. Контекст — это интерфейс, а указатели на интерфейсы почти всегда являются ошибкой. Кроме того, из документации пакета: «Программы, использующие контексты, должны следовать этим правилам, чтобы обеспечить согласованность интерфейсов между пакетами и включить инструменты статического анализа для проверки распространения контекста: [...] Контекст должен быть первым параметром, обычно называемым ctx».