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 문의 실행은 여러 단계로 진행돼요. 하나씩 따라가 볼게요.

  1. 문장에 있는 모든 case에 대해, receive 연산의 채널 피연산자와 send 문의 채널 및 오른쪽 표현식을 select 문에 들어가는 순간 소스 순서대로 정확히 한 번 평가해요. 그 결과로 받거나 보낼 채널 집합과 보낼 값들이 정해져요. 이 평가에서 발생하는 부수 효과(side effect)는 어떤 통신 연산이 선택되든 간에 모두 일어나요. 짧은 변수 선언이나 할당이 있는 RecvStmt의 왼쪽 표현식은 아직 평가되지 않아요.

  2. 진행될 수 있는 통신이 하나 이상 있다면, 그중 하나를 균일한 의사 난수(uniform pseudo-random) 선택으로 고릅니다. 아니라면 default case가 있으면 그 case가 선택돼요. default case도 없다면, 통신 중 적어도 하나가 진행될 수 있을 때까지 select 문은 블록돼요.

  3. 선택된 case가 default case가 아니라면, 해당 통신 연산이 실행돼요.

  4. 선택된 case가 짧은 변수 선언이나 할당이 있는 RecvStmt라면, 왼쪽 표현식을 평가하고 받은 값(들)을 할당해요.

  5. 선택된 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에 아무 문장도 없이 c0 또는 1을 보내기만 해요. 실제로 진행될 수 있는 case 중 하나가 무작위로 뽑히므로, 결과적으로 임의의 비트 시퀀스를 채널 c에 보내는 셈이에요. 마지막 select {}는 case가 아예 없어서 영원히 블록되는 대표적인 예시예요—고루틴을 의도적으로 멈춰 둘 때 이런 형태를 쓰기도 해요.

더 알아보기

  • 통신의 반대편인 값을 보내는 문법은 Send statements에서 다뤄요. receive 쪽과 함께 보면 채널 통신의 양면이 잡혀요.
  • 값을 받아오는 연산 자체는 Receive operator에서 더 자세히 볼 수 있어요.
  • nil 채널에서 통신이 왜 진행되지 않는지, 그래서 select {}가 어떤 상황에서 유용한지 이해하면 채널 기반 동시성 코드를 짤 때 실수가 줄어요.