여러 채널 기다리기
select는 switch처럼 생겼지만 각 case가 채널 송신이나 수신입니다. 진행할 수 있는 case가 생길 때까지 블록되었다가 그 case를 실행합니다.
이 지연 시간이라면 첫 번째 select는 빠른 결과를, 두 번째는 느린 결과를 받습니다. 어느 수신도 다른 채널을 기다릴 필요가 없습니다. 그냥 <-slow 다음에 <-fast를 쓰면 무엇이 먼저 도착하든 정해진 순서로 처리하게 됩니다.
select가 평가되는 방식:
select가 시작될 때 모든 채널 표현식과 보낼 값이 소스 순서대로 한 번씩 평가됩니다.- 준비된 case가 하나 이상이면 그중 하나가 무작위로 선택됩니다.
- 준비된 것이 없고
default가 있으면default가 실행됩니다. - 그렇지 않으면 고루틴은 어떤 case가 준비될 때까지 블록됩니다.
빈 select {}는 영원히 블록됩니다. 실제 작업이 다른 고루틴에서 일어나는 프로그램의 main 끝에서 가끔 볼 수 있습니다.
준비된 case 중 무작위 선택
여러 case가 동시에 준비되면 select는 먼저 나열된 case를 선호하지 않습니다. 이 프로그램은 버퍼가 있는 채널 두 개를 채운 뒤 1000번 select합니다:
countA와 countB의 비율은 실행할 때마다 바뀌며 각각 500 근처에 머뭅니다. 무작위 선택은 의도된 것으로, 바쁜 채널이 다른 채널을 굶기지 못하게 합니다. 우선순위가 필요하다면 아래에 나오는 패턴을 보세요.
default로 블록되지 않는 연산
default case가 있으면 select는 절대 블록되지 않습니다. 송신이나 수신을 "시도" 연산으로 바꿔 줍니다:
버퍼가 가득 찼을 때 작업을 버리는 것은 호출자를 멈추게 하지 않으면서 부하를 덜거나 지표를 내보내는 방법입니다.
채널을 반복해서 "확인"하려고 for 반복문 안의 select에 default를 넣지 마세요. 준비된 것이 없으면 반복문이 CPU를 100% 쓰며 헛돕니다. 대신 블록되게 하고, 주기적으로 깨어나야 한다면 타임아웃 case를 추가하세요.
타임아웃
time.After(d)는 d가 지난 뒤 한 번 값을 받는 채널을 반환합니다. 실제 작업과 경주시키세요:
첫 번째 호출은 "data"를 반환합니다. 두 번째 호출은 워커가 끝나기 한참 전인 50ms 후에 타임아웃 오류를 반환합니다. 결과 채널에는 일부러 버퍼 1을 두었습니다. 타임아웃이 이기면 아무도 result에서 받지 않는데, 버퍼 없는 채널이라면 워커 고루틴이 송신에서 영원히 블록되어 누수가 생깁니다.
반복문 안에서 time.After는 반복마다 새 타이머를 만들며, 메시지마다의 유휴 타임아웃("1초 동안 메시지 없음")에는 바로 그것이 맞습니다. 여러 연산에 걸친 전체 마감 시간이라면 반복문 전에 타이머나 컨텍스트를 하나 만드세요. Go 1.23부터는 더 이상 참조되지 않는 타이머가 발동 전이라도 가비지 컬렉션되므로, 반복문 안의 time.After가 예전 버전처럼 각 타이머가 발동할 때까지 메모리를 붙잡지 않습니다(go.mod에 go 1.23 이상이 필요합니다).
for-select 반복문과 종료 채널
멈추라는 말을 들을 때까지 실행되는 고루틴은 작업용 case 하나와 멈춤용 case 하나가 있는 select를 for 반복문으로 감싼 형태입니다:
quit에 값을 보내는 대신 닫는 것이 관용구입니다. 닫힘은 지금과 나중의 모든 수신자에게 보이므로, close 한 번으로 워커를 몇 개든 멈출 수 있습니다. done 채널은 main이 워커가 실제로 반환될 때까지 기다리게 해 줍니다.
실제 코드에서 종료 채널은 보통 context.Context입니다: case <-ctx.Done():. 같은 방식으로 동작하며(Done()은 취소 시 닫히는 채널을 반환합니다) 마감 시간과 멈춘 이유도 함께 전달합니다. 컨텍스트 페이지에서 다룹니다.
select 안의 break
select case 안의 break는 감싸는 for가 아니라 select를 빠져나갑니다. 끝나지 않는 반복문의 흔한 원인입니다. return을 쓰거나 반복문에 레이블을 붙이세요:
채널 사이의 우선순위
select는 무작위로 고르므로 한 문장 안에서 case에 순위를 매길 수 없습니다. 어떤 채널에 값이 있을 때마다 그 채널이 이기게 하려면 먼저 그것만 따로 확인하세요:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
첫 번째 select는 취소가 이미 일어났다면 즉시 반환합니다. 이것이 없으면 ctx가 취소된 뒤에도 꾸준히 들어오는 작업이 한동안 무작위 선택에서 계속 이길 수 있습니다.
nil 채널은 case를 끈다
nil 채널에 대한 송신이나 수신은 절대 준비되지 않으므로, nil 채널에 대한 case는 사실상 꺼집니다. 닫힌 입력 채널을 nil로 설정하면 나머지 채널은 계속 select하면서 그 채널만 제외할 수 있습니다. 채널 페이지에 이 방식으로 만든 병합 반복문이 있습니다. 같은 요령으로 타임아웃을 켜고 끌 수도 있습니다. var timeout <-chan time.Time을 필요할 때까지 nil로 두었다가 time.After(d)를 대입하면 됩니다.
흔한 실수
- 소스 순서를 기대함. 먼저 나열된 case가 우선되지 않습니다.
default로 바쁜 반복. 할 일이 없는for { select { ... default: } }는 CPU 코어 하나를 태웁니다.- 진 쪽을 누수시킴. 타임아웃이 이겨도 결과를 보내려던 고루틴은 끝날 수 있어야 합니다. 그 채널에 버퍼 1을 주세요.
select만 빠져나가는break. 레이블이나return을 쓰세요.- 반복마다 만드는
time.After를 전체 마감 시간으로 씀. 반복마다 다시 시작됩니다. 마감 시간은 반복문 밖에서 한 번 만드세요.
자주 묻는 질문
Go에서 select는 무엇을 하나요?
select는 채널 연산(송신 또는 수신) 중 하나가 진행될 수 있을 때까지 기다렸다가 그 case를 실행합니다. 여러 개가 동시에 준비되면 무작위로 하나를 고릅니다. default case가 없으면 어떤 case가 준비될 때까지 블록되고, default가 있으면 절대 블록되지 않습니다.
Go에서 채널 수신에 타임아웃을 두려면 어떻게 하나요?
수신과 타이머를 하나의 select에 넣습니다: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. 먼저 일어나는 쪽이 이깁니다. 반복문 안이거나 호출자에게 이미 마감 시간이 있다면, 대신 context.WithTimeout으로 만든 context.Context를 쓰고 ctx.Done()을 select하세요.
Go의 select는 case를 순서대로 고르나요?
아니요. 둘 이상의 case가 준비되면 Go는 균등하게 무작위로 고르므로, 어떤 case도 다른 case를 굶기지 못합니다. 우선순위가 필요하다면 먼저 우선순위가 높은 채널을 default가 있는 별도의 select로 확인한 뒤, 모든 채널에 대한 select로 넘어가세요.
왜 break로 for-select 반복문을 빠져나가지 못하나요?
select 안에서 break는 감싸는 for가 아니라 select 문만 빠져나갑니다. return을 쓰거나 반복문에 레이블을 붙이세요(loop: for { select { case <-done: break loop } }).