select 문
select 문 (Select statements)
통신 연산 여러 개 중에서 하나를 골라 진행시키는 문장이에요. 마치 switch처럼 보이지만, 각 case가 값 비교가 아니라 send나 receive 같은 통신 연산을 가리킨다는 점이 핵심이에요. 고루틴 사이에서 여러 채널을 동시에 기다려야 할 때 이 문장이 큰 역할을 하죠. 어떻게 동작하고 어떤 경우에 쓰는지 순서대로 살펴볼게요.
출처: Go Specification
본문
select 문은 가능한 send 또는 receive 연산들의 집합 중에서 어떤 것이 진행될지 선택해요. 문법이 switch 문과 비슷한데, case들이 모두 통신 연산을 가리킨다는 점이 다르죠.
SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr = Expression .
RecvStmt를 가진 case는 RecvExpr의 결과를 변수 하나 또는 둘에 할당할 수 있어요. 이때 변수는 **짧은 변수 선언(short variable declaration)**으로 선언할 수도 있고요. RecvExpr는 (괄호로 감싼 것도 허용되는) receive 연산이어야 해요. default case는 최대 하나만 있을 수 있고, case 목록의 어느 위치에든 올 수 있어요.
select 문의 실행은 여러 단계로 진행돼요. 하나씩 따라가 볼게요.
-
문장에 있는 모든 case에 대해, receive 연산의 채널 피연산자와 send 문의 채널 및 오른쪽 표현식을
select문에 들어가는 순간 소스 순서대로 정확히 한 번 평가해요. 그 결과로 받거나 보낼 채널 집합과 보낼 값들이 정해져요. 이 평가에서 발생하는 부수 효과(side effect)는 어떤 통신 연산이 선택되든 간에 모두 일어나요. 짧은 변수 선언이나 할당이 있는RecvStmt의 왼쪽 표현식은 아직 평가되지 않아요. -
진행될 수 있는 통신이 하나 이상 있다면, 그중 하나를 균일한 의사 난수(uniform pseudo-random) 선택으로 고릅니다. 아니라면
defaultcase가 있으면 그 case가 선택돼요.defaultcase도 없다면, 통신 중 적어도 하나가 진행될 수 있을 때까지select문은 블록돼요. -
선택된 case가
defaultcase가 아니라면, 해당 통신 연산이 실행돼요. -
선택된 case가 짧은 변수 선언이나 할당이 있는
RecvStmt라면, 왼쪽 표현식을 평가하고 받은 값(들)을 할당해요. -
선택된 case의 문장 목록(statement list)이 실행돼요.
이제 예시를 볼게요. nil 채널에서는 통신이 결코 진행될 수 없으니, nil 채널만 있고 default case가 없는 select는 영원히 블록돼요. 아래 예시는 여러 채널에서 값을 받거나 보낼 때 분기하는 전형적인 모습이에요.
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}
for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}
select {} // block forever
예시를 조금 풀어볼게요. 첫 번째 select는 다섯 개 채널(c1~c4, c)과 default를 두고 있어요. 특정 case가 바로 진행될 수 없으면 default가 선택되어 "no communication"을 출력하죠. 두 번째 select는 case에 아무 문장도 없이 c에 0 또는 1을 보내기만 해요. 실제로 진행될 수 있는 case 중 하나가 무작위로 뽑히므로, 결과적으로 임의의 비트 시퀀스를 채널 c에 보내는 셈이에요. 마지막 select {}는 case가 아예 없어서 영원히 블록되는 대표적인 예시예요—고루틴을 의도적으로 멈춰 둘 때 이런 형태를 쓰기도 해요.
더 알아보기
- 통신의 반대편인 값을 보내는 문법은 Send statements에서 다뤄요. receive 쪽과 함께 보면 채널 통신의 양면이 잡혀요.
- 값을 받아오는 연산 자체는 Receive operator에서 더 자세히 볼 수 있어요.
nil채널에서 통신이 왜 진행되지 않는지, 그래서select {}가 어떤 상황에서 유용한지 이해하면 채널 기반 동시성 코드를 짤 때 실수가 줄어요.