숏컷 코드
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
if err := callBackend(ctx); err != nil {
return err
}ctx, cancel := context.WithCancelCause(parent)
cancel(fmt.Errorf("worker stopped: %w", err))
<-ctx.Done()
return context.Cause(ctx)취소 경계
deadline과 timeout은 작업 수명 상한을 정한다
context.WithTimeout은 현재 시점부터 상대 시간을, context.WithDeadline은 절대 시각을 기준으로 child context를 만듭니다. 둘 다 시간이 지나면 ctx.Done()이 닫히고 ctx.Err()는 보통 context.DeadlineExceeded가 됩니다.
func fetchUser(ctx context.Context, id string) (User, error) {
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/users/"+id, nil)
if err != nil {
return User{}, err
}
return send(req)
}반환받은 cancel은 timeout 전에 작업이 끝나도 호출해야 합니다. timer와 parent-child 참조를 정리하기 위해 defer cancel()을 붙이는 패턴이 기본입니다.
cancel cause는 "취소됨" 이상의 원인을 남긴다
ctx.Err()는 보통 context.Canceled나 context.DeadlineExceeded처럼 취소 종류를 알려 줍니다. 어느 단계에서 어떤 이유로 취소했는지까지 전달하려면 context.WithCancelCause와 context.Cause를 씁니다.
func runWorker(ctx context.Context) error {
ctx, cancel := context.WithCancelCause(ctx)
go func() {
if err := process(ctx); err != nil {
cancel(fmt.Errorf("process failed: %w", err))
return
}
cancel(nil)
}()
<-ctx.Done()
if cause := context.Cause(ctx); cause != nil {
return cause
}
return ctx.Err()
}WithCancelCause의 cancel 함수는 error를 받습니다. 반면 WithTimeoutCause와 WithDeadlineCause는 timeout이나 deadline이 실제로 발생했을 때 지정한 cause를 기록하고, 반환된 CancelFunc 자체는 cause를 설정하지 않습니다.
context는 값 저장소가 아니라 요청 수명 경계다
context는 deadline, 취소 신호, request-scoped value를 API 경계로 전파하는 타입입니다. optional parameter나 서비스 설정을 담는 struct처럼 쓰면 호출 관계가 흐려지고 테스트도 어려워집니다.
func QueryUser(ctx context.Context, db *sql.DB, id string) (User, error) {
row := db.QueryRowContext(ctx, "select id, name from users where id = ?", id)
// ...
return user, row.Scan(&user.ID, &user.Name)
}관례적으로 context.Context는 첫 번째 매개변수로 둡니다. nil context를 넘기지 말고 아직 적절한 parent가 없으면 context.TODO()를 사용합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 사용자가 요청을 취소할 수 있음 | parent ctx 전파 |
| 작업 시간 상한이 필요함 | WithTimeout 또는 WithDeadline |
| 취소 원인까지 추적해야 함 | WithCancelCause + context.Cause |
| timeout 자체에 원인 라벨이 필요함 | WithTimeoutCause |
| 설정값이나 선택 인자를 전달함 | context가 아니라 명시 매개변수 |
HTTP handler, DB query, 외부 API 호출은 context를 연결해야 요청 취소와 timeout이 아래 계층까지 전파됩니다. 반대로 CPU bound loop는 라이브러리가 자동으로 멈춰 주지 않으므로 반복 중간에 ctx.Err()나 select로 직접 확인해야 합니다.
주의할 점
WithTimeout, WithDeadline, WithCancel이 반환한 cancel 함수를 버리면 child context와 timer가 parent 취소 전까지 남을 수 있습니다.
작업이 정상 종료되어도 defer cancel()로 정리하는 습관을 유지해야 합니다.
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
if err := do(ctx); err != nil {
return err
}context를 struct 필드에 저장하면 요청마다 다른 취소 경계를 표현하기 어렵습니다. 서비스 객체에는 dependency를 저장하고, context는 호출마다 명시적으로 전달하는 편이 안전합니다.
참고 링크
1 sources