숏컷 코드
for {
select {
case item, ok := <-jobs:
if !ok {
return
}
handle(item)
case <-ctx.Done():
return
}
}문법
select는 여러 channel 작업 중 준비된 하나를 실행한다
select는 여러 channel send/receive 후보를 동시에 기다립니다. 준비된 case가 하나면 그 case를 실행하고, 여러 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이 필요한지, 취소 신호를 기다릴지 먼저 구분합니다.
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해야 합니다.
종료 설계
| 상황 | 먼저 고를 방식 |
|---|---|
| 값 생산 완료 알림 | 송신자가 close(ch) |
| 요청 취소 전파 | ctx.Done() |
| 여러 입력 중 먼저 온 것 처리 | select |
| 계속 작업 루프 유지 | for + select |
| non-blocking 시도 | select + default |
취소 신호와 channel close는 목적이 다릅니다. close는 특정 데이터 흐름의 종료이고, context 취소는 작업 전체의 수명 종료입니다. 둘을 같은 의미로 쓰면 종료 소유권이 흐려집니다.
주의할 점
channel은 아무 곳에서나 닫는 자원이 아닙니다. 일반적으로 값을 보내는 쪽이 닫고, 받는 쪽은 ok 또는 range로 종료를 감지합니다. 닫힌 channel에 send하면 panic이 발생하므로, 여러 송신자가 있는 구조에서는 close 지점을 하나로 모아야 합니다.
nil channel은 send와 receive가 영원히 대기합니다. select 안에서 특정 case를 일시적으로 비활성화할 때 의도적으로 nil을 쓰기도 하지만, 초기화 누락으로 nil channel이 들어가면 작업이 멈춘 것처럼 보입니다.
참고 링크
3 sources