Quick Syntax
for {
select {
case item, ok := <-jobs:
if !ok {
return
}
handle(item)
case <-ctx.Done():
return
}
}select {
case results <- result:
return nil
case <-ctx.Done():
return ctx.Err()
}receive뿐 아니라 send도 소비자가 멈출 수 있으므로 ctx.Done()과 경쟁시킵니다. channel close는 데이터 생산 종료이고 context 취소는 작업 수명 종료입니다.
문법
select는 여러 channel 작업 중 준비된 하나를 실행한다
select는 여러 channel send/receive 후보를 동시에 기다립니다. 준비된 case가 하나면 그 case를 실행하고, 여러 case가 준비되어 있으면 그중 하나를 균등한 pseudo-random 방식으로 선택합니다. 특정 case가 먼저 처리된다는 우선순위 보장은 없습니다. 어떤 case도 준비되지 않았고 default도 없으면 현재 goroutine은 대기합니다.
select {
case msg := <-messages:
fmt.Println(msg)
case err := <-errors:
return err
case <-ctx.Done():
return ctx.Err()
}select는 반복문이 아닙니다. 계속 기다리려면 for와 함께 써야 합니다. 한 번만 기다릴 것인지, 작업 루프를 돌릴 것인지가 코드 구조를 결정합니다.
default는 non-blocking 선택을 만든다
default가 있으면 준비된 channel 작업이 없을 때 바로 실행됩니다. 상태 확인, best-effort send, 빠른 fallback에는 유용하지만, 루프 안에서 무조건 default를 돌리면 CPU를 계속 쓰는 busy loop가 될 수 있습니다.
select {
case queue <- item:
// 보냄
default:
// 지금은 받을 쪽이 없으므로 버림 또는 재시도 예약
}대기해야 하는 작업에 default를 넣으면 작업이 너무 빨리 포기될 수 있습니다. non-blocking이 필요한지, timeout이 필요한지, 취소 신호를 기다릴지 먼저 구분합니다.
send도 취소와 함께 끝낸다
func publish(ctx context.Context, out chan<- Result, result Result) error {
select {
case out <- result:
return nil
case <-ctx.Done():
return ctx.Err()
}
}worker가 result를 보내는 동안 receiver가 먼저 return하면 단순 out <- result는 영원히 막힐 수 있습니다. receive loop만 취소를 검사하지 말고, block될 수 있는 send에도 같은 종료 경계를 둡니다. for range ch는 channel이 닫힐 때만 끝나므로 context 취소로도 중단해야 하면 for와 select를 씁니다.
close는 "더 이상 보낼 값이 없다"는 송신자 신호다
channel close는 receiver에게 값 생산이 끝났음을 알리는 신호입니다. 닫힌 channel에서 receive하면 남은 버퍼 값을 모두 읽은 뒤 zero value와 ok=false를 받습니다. 그래서 for range ch는 channel이 닫힐 때까지 값을 읽고 자연스럽게 종료됩니다.
for item := range jobs {
handle(item)
}보통 channel을 닫는 주체는 송신자입니다. receiver가 닫으면 다른 송신자가 send하는 순간 panic이 발생할 수 있습니다. 여러 goroutine이 같은 channel에 send한다면, 모든 송신자가 끝난 뒤 한 곳에서만 close해야 합니다. 닫힌 channel을 다시 닫거나 send하면 panic이고 nil channel을 닫아도 panic입니다. ctx.Done()은 context 소유자가 닫는 신호이므로 호출자가 close하지 않습니다.
종료 설계
| 상황 | 먼저 고를 방식 |
|---|---|
| 값 생산 완료 알림 | 송신자가 close(ch) |
| 요청 취소 전파 | ctx.Done() |
| 여러 입력 중 먼저 온 것 처리 | select |
| 계속 작업 루프 유지 | for + select |
| non-blocking 시도 | select + default |
취소 신호와 channel close는 목적이 다릅니다. close는 특정 데이터 흐름의 종료이고, context 취소는 작업 전체의 수명 종료입니다. 둘을 같은 의미로 쓰면 종료 소유권이 흐려집니다.
주의할 점
channel은 아무 곳에서나 닫는 자원이 아닙니다. 일반적으로 값을 보내는 쪽이 닫고, 받는 쪽은 ok 또는 range로 종료를 감지합니다. 닫힌 channel에 send하거나 close하면 panic이 발생하므로, 여러 송신자가 있는 구조에서는 close 지점을 하나로 모아야 합니다.
nil channel은 send와 receive가 영원히 대기합니다. select 안에서 특정 case를 일시적으로 비활성화할 때 의도적으로 nil을 쓰기도 하지만, 초기화 누락으로 nil channel이 들어가면 작업이 멈춘 것처럼 보입니다.
참고 링크
3 sources