Go는 동시성에 특화된 언어적 특성을 가지고 있다. 전통적인 방식의 언어에서는 그린 스레드(또는 OS 레벨의 스레드)를 만들고 공유 변수와 메모리 락을 통해 동시성을 제어하는 편이다. Go는 고루틴을 사용하고 이는 그린 스레드 개념보다 훨씬 가볍고 작다. 단순히 함수의 흐름(continuation이라고 표현한다)을 호스팅 하는 역할을 한다. 내부적으로 GOMAXPROCS 변수의 크기만큼 OS 레벨의 스레드를 만들고 그 수 만큼 논리 프로세서를 만들어 이 고루틴을 멀티플랙싱 하는 방식이다. 관련된 내용은 스케줄러 파트에서 자세히 다룬다. 어쨌든 Go는 이렇게 동시 실행되는(정확히는 순서를 알 수 없는) 고루틴을 동기화 하기 위해서 위에서 언급한 “전통적인 방식"을 제공하지만, 채널이라는 특수한 연결 고리 역시 제공하고 있다. 채널을 공유해서 데이터 흐름을 강제할 수 있다.
채널 #
고루틴은 Go 프로그램의 동작이라고 볼 수 있고, 채널은 고루틴 간의 연결이다. 한 고루틴에서 다른 고루틴으로 값을 보내기 위한 통신 메커니즘이다.
생성과 채널 타입 #
make 내장 함수를 통해 만들 수 있다. 예를 들어서 아래 코드는 int 자료형을 주고 받는 통로 역할을 하는 채널을 만들어준다.
ch := make(chan int)
ch는 맵 자료 구조처럼, make로 인해 만들어진 데이터 구조에 대한 참조이다. 채널을 복사하거나, 함수의 인자로 전달할 때는 참조를 통해 복사하기 때문에 복사된 값은 같은 데이터 구조를 참조하게 된다. 다른 참조 타입들과 마찬가지로 nil값을 제로값으로 갖게 된다. 같은 타입인 채널은 비교 연산으로 비교가 가능하다.
c1 := make(chan int)
c2 := make(chan int)
c3 := make(chan string)
fmt.Println(c1 == c2) // false, 타입은 같기 때문에 비교 연산은 가능한데, 같은 채널이 아님
fmt.Println(c3 == c2) // 컴파일 에러, 타입이 다름
var nilChan1 chan int
var nilChan2 chan int
fmt.Println(nilChan1 == nilChan2) // true, 타입도 같은데 둘 다 nil값으로 같음
수신 타입, 송신 타입 #
채널은 데이터를 주고 받는 역할을 한다고 했다. 송신 구문은 하나의 고루틴에서 채널을 통해 그 채널의 수신 표현식이 있는 다른 고루틴으로 값을 전달한다. 송신과 수신 모두 <- 연산을 사용하는데, 붙는 위치에서 차이가 발생한다.
ch <- x // 송신
y := <-ch // 수신
송신하는 역할을 하는 채널과 수신하는 역할을 하는 채널은 다른 타입으로 분류가 된다.
c := make(chan int)
var c1 chan <- int // 송신용 채널 타입
var c2 <-chan int // 수신용 채널 타입
c1 = c
c2 = c
// c1 != c2 둘 비교는 컴파일 에러를 발생시킨다. (mismatched types)
fmt.Println(c1 == c) // true,
fmt.Println(c2 == c) // true, 양방향 모두 가능한 채널은 각 타입으로 암묵적인 변환이 가능하다.
송신 전용 채널에서 수신을 하려 한다든지, 수신 전용 채널에서 송신을 하려 하면 컴파일 에러(invalid operation)를 발생시킨다. 이 특징을 사용해서 함수 내에서 기능을 제한하는 용도로 사용해 더 안전하게 프로그램을 작성할 수 있다.
func writer(out <-chan int) {
var results []int
// ...
for _, data := results {
out <- data
}
}
func main() {
ch := make(chan int)
// ...
writer(ch)
}
한 번 구체적인 용도가 정해진 채널을 다시 바꿀 수는 없다.
버퍼 #
채널은 버퍼가 있는 채널과 없는 채널로 나눠진다. 버퍼를 설정하기 위해서는 make 내장 함수의 두 번째 인자값으로 음이 아닌 정수를 넣어주면 된다.
c1 := make(chan int) // make(chan int, 0)과 같다.
c2 := make(chan int, 2) // 버퍼가 2인 채널
버퍼
송신 #
송신은 위에서 언급한 타입대로
채널을 닫기 위해서는 close 내장 함수를 사용한다. close를 사용해 송신 가능한 채널을 닫으면, 더 이상 값을 보내지 않음을 나타내는 플래그를 설정한다. 이후 송신을 시도하면 패닉이 발생한다. 그리고, 만약 닫혀있는 채널을 다시 닫으려고 하면 마찬가지로 패닉이 발생된다.
panic: close of closed channel # 닫힌 채널을 다시 닫으려고 할 때
panic: send on closed channel # 닫힌 채널에 데이터를 송신하려고 할 때