Управление временем жизни (context)
Представьте, что вы делаете заказ в ресторане. Официант (основной процесс) идет к повару (горутина 1), повар просит мясника (горутина 2) отрезать мясо. Если вы вдруг передумали и ушли из ресторана, официант должен сказать всем: "Стоп! Заказ отменен, не тратьте время".
context.Context — это как раз этот "сигнал отмены", который передается по цепочке вызовов.
1. Зачем нужен context?
У него две основные задачи:
- Отмена (Cancellation): Остановить выполнение горутин, если результат работы больше не нужен (например, таймаут или отмена пользователем).
- Передача данных (Request Scoping): Передать через цепочку функций какую-то информацию, актуальную для одного конкретного запроса (например, ID пользователя, токен авторизации или ID трассировки).
2. Как работает context?
Контекст — это иммутабельный объект (неизменяемый). Когда вы хотите "добавить" к контексту что-то новое (например, таймаут), вы не меняете старый, а создаете новый, который "держится" за старый.
Типы контекстов:
context.Background(): Базовый контекст. Используется в функцииmainили как корневой контекст для тестов.context.WithCancel(parent): Возвращает новый контекст и функциюcancel(). Вызовcancel()пошлет сигнал всем горутинам, использующим этот контекст.context.WithTimeout(parent, duration): То же самое, чтоWithCancel, но отмена произойдет автоматически через указанное время.
3. Пример: Таймаут для долгой задачи
Представьте, что вы делаете запрос к базе данных, который не должен длиться дольше 2 секунд.
func processData(ctx context.Context) {
select {
case <-time.After(5 * time.Second): // Имитация долгой работы
fmt.Println("Задача выполнена!")
case <-ctx.Done(): // Сигнал, если время вышло или мы сами вызвали cancel
fmt.Println("Задача отменена:", ctx.Err())
}
}
func main() {
// Создаем контекст с таймаутом в 2 секунды
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Всегда вызывайте cancel, чтобы освободить ресурсы!
processData(ctx)
}
4. Как правильно использовать context
Существует несколько «золотых правил» использования контекста в Go:
- Контекст первым аргументом: Если ваша функция принимает контекст, он всегда должен быть первым аргументом.
func DoSomething(ctx context.Context, arg1 int) error { ... } - Не храните контекст в структурах: Контекст — это про запрос, а не про объект. Он должен "течь" сквозь функции.
- Используйте
context.TODO(): Если вы еще не знаете, какой контекст передать, используйтеTODO()— это сигнал другим разработчикам, что здесь должен быть реальный контекст. defer cancel(): При использованииWithCancelилиWithTimeoutвсегда вызывайтеcancelсразу после создания (черезdefer). Это освобождает ресурсы памяти, связанные с контекстом.
5. Передача значений (Request-Scoped Values)
Иногда нужно передать ID пользователя через 10 слоев функций, не пробрасывая его в каждом аргументе.
// Установка значения
ctx := context.WithValue(context.Background(), "userID", 123)
// Чтение значения
if val, ok := ctx.Value("userID").(int); ok {
fmt.Println("ID пользователя:", val)
}
Важно: Не используйте WithValue для передачи обычных аргументов функций! Это "плохой тон", так как это делает код запутанным и лишает его статической типизации. Используйте это только для технических данных (логирование, авторизация, трейсинг).
Итого: когда применять?
- Если ваша функция выполняет сетевой запрос или длительные вычисления — она обязана принимать
context.Context. - Если вам нужно ограничить время выполнения задачи — используйте
context.WithTimeout. - Если вы пишете web-сервер —
net/httpуже предоставляет контекст запроса, который нужно передавать дальше по цепочке функций.