Пакет Context у Go у 2026 році: Скасування, Таймаути та Питання на Співбесідах

Повний посібник з пакету context у Go - від інтерфейсу Context через WithCancel та WithTimeout до просунутих патернів та питань на технічних співбесідах.

Пакет Context у Go - скасування, таймаути та найкращі практики

Пакет context у Go забезпечує стандартний механізм передачі дедлайнів, сигналів скасування та значень, пов'язаних із запитом, через межі API. Кожен продакшн-застосунок на Go використовує context: HTTP-обробники, запити до бази даних, виклики gRPC та фонові воркери залежать від нього для коректного завершення роботи та керування таймаутами.

Важливо для співбесіди

Context є однією з найчастіших тем на співбесідах з Go. Інтерв'юери очікують, що кандидати пояснять поширення контексту, продемонструють правильну обробку скасування та уникнуть типових помилок, таких як зберігання контексту в структурах.

Інтерфейс Context та Його Чотири Методи

Інтерфейс Context визначає чотири методи, які повинна реалізувати кожна імплементація контексту. Розуміння цих методів формує основу для ефективного використання контексту.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() повертає канал тільки для читання. Коли цей канал закривається, будь-яка горутина, що його слухає, миттєво отримує сигнал. Патерн <-ctx.Done() блокується до моменту скасування. Err() пояснює причину: або context.Canceled при явному скасуванні, або context.DeadlineExceeded коли минув таймаут або дедлайн.

Створення Контекстів за Допомогою Background та TODO

Дві функції створюють кореневі контексти: context.Background() та context.TODO(). Обидві повертають непорожні контексти, які ніколи не скасовуються.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() служить батьківським елементом для вхідних запитів, функцій main та коду ініціалізації. TODO() діє як плейсхолдер під час рефакторингу, коли правильне джерело контексту ще не визначено. Інструменти статичного аналізу можуть позначати використання TODO() для перевірки.

Скасування за Допомогою WithCancel та WithCancelCause

Ручне скасування дозволяє батьківським горутинам сигналізувати дочірнім, що робота повинна бути зупинена. Цей патерн з'являється в пулах воркерів, фонових задачах та імплементаціях graceful shutdown.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, представлений у Go 1.20, надає багатшу інформацію про помилки, ніж звичайний WithCancel. Функція context.Cause() отримує помилку, передану функції cancel, що полегшує дебагінг у складних системах.

Готовий до співбесід з Go?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Таймаути та Дедлайни для Обробки Запитів

HTTP-обробники, виклики бази даних та запити до зовнішніх API завжди повинні мати таймаути. Пакет context пропонує WithTimeout та WithDeadline для цієї мети. Обидва створюють контексти, які автоматично скасовуються після закінчення часу.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

Патерн defer cancel() забезпечує звільнення ресурсів навіть коли операція завершується до таймауту. Пропуск цього виклику спричиняє витік ресурсів: горутина таймера контексту залишається активною до закінчення таймауту.

Значення, Пов'язані із Запитом, за Допомогою WithValue

WithValue прикріплює дані, пов'язані із запитом, до контексту. Типові випадки використання включають ідентифікатори запитів, токени автентифікації та спани трейсингу. Офіційна документація наголошує на використанні власних типів як ключів для уникнення колізій.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Значення контексту формують незмінний ланцюг: кожен виклик WithValue створює новий контекст, що обгортає батьківський. Пошуки проходять через цей ланцюг, що робить глибоко вкладені значення повільнішими для доступу. Слід зберігати лише дані, пов'язані із запитом, а не конфігурацію застосунку чи необов'язкові параметри.

Утиліти WithoutCancel та AfterFunc

Go 1.21 додав WithoutCancel для операцій, які повинні завершитися незалежно від скасування батьківського контексту, таких як задачі очищення. AfterFunc планує callback, коли відбувається скасування контексту.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel зберігає значення з батьківського контексту, але ігнорує його сигнал скасування. Цей патерн підходить для логування, емісії метрик та комітів у базу даних, які повинні успішно завершитися незалежно від результату запиту.

Питання та Відповіді для Співбесіди

Підготовка до технічної співбесіди з Go вимагає розуміння внутрішньої роботи контексту та найкращих практик. Ці питання часто з'являються на співбесідах у компаніях, що використовують Go для бекенд-сервісів. Більше матеріалів для підготовки до співбесід з Go можна знайти в модулі питань про пакет Context.

Чому context повинен бути першим параметром?

Команда Go встановила цю конвенцію, щоб поширення контексту було видимим та послідовним. Розміщення контексту першим сигналізує читачам, що функція поважає скасування. Стандартна бібліотека дотримується цього патерну: http.Request.Context(), database/sql.QueryContext() та grpc.UnaryInterceptor всі очікують контекст як перший аргумент.

Що станеться, якщо зберегти context у структурі?

Зберігання контексту порушує модель життєвого циклу запиту. Структура може пережити запит, для якого вона була створена, спричиняючи використання операціями застарілих сигналів скасування або пропуск нових дедлайнів. Документація контексту явно попереджає про цей патерн.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

Як обробляти context у горутинах?

Горутини повинні перевіряти ctx.Done() у своєму основному циклі або операторі select. Ігнорування каналу done створює витоки горутин: батьківська функція повертається, але горутина продовжує споживати ресурси.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

Коли слід використовувати context.TODO()?

Слід використовувати TODO() під час поступового рефакторингу при додаванні підтримки контексту до застарілого коду. Це позначає місця, які потребують правильного поширення контексту. Продакшн-код повинен врешті-решт замінити всі виклики TODO() реальними контекстами з обробників запитів або ініціалізації застосунку. Більше про патерни конкурентності в Go можна дізнатися зі статті Go Concurrency: Goroutines and Channels.

Типові Помилки, Яких Слід Уникати

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

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Ключові Висновки щодо Використання Go Context у Продакшні

  • Контекст слід передавати як перший параметр функцій, ніколи не зберігати його в структурах
  • Завжди слід викликати defer cancel() після WithTimeout, WithDeadline або WithCancel для запобігання витокам ресурсів
  • Використання WithCancelCause та context.Cause() покращує дебагінг помилок у складних ланцюгах скасування
  • Перевірка <-ctx.Done() у циклах горутин забезпечує коректне завершення та запобігає витокам
  • Власні типи ключів для WithValue дозволяють уникнути колізій між пакетами
  • WithoutCancel слід застосовувати для операцій очищення, які повинні завершитися незалежно від скасування батьківського контексту
  • Перевага віддається context.Background() для ініціалізації застосунку та context.TODO() лише під час рефакторингу
Щоденний виклик

Чи знайдеш ти помилку в Go?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 29 серпня 2026 р.

Теги

#go
#context
#concurrency
#best-practices
#interview

Поділитися

Пов'язані статті