보내기와 받기
채널은 고루틴 사이의 타입이 있는 파이프입니다. ch <- v는 보내고, <-ch는 받습니다. make로 만듭니다:
채널 타입의 제로 값은 nil이므로 make 없이 var ch chan string이라고 쓰면 영원히 블록되는 채널을 얻습니다. 채널은 항상 make로 만드세요.
버퍼 없는 채널은 동기화한다
make(chan T)는 버퍼 없는 채널을 만듭니다. 송신은 수신자가 값을 가져갈 때까지 블록되고, 수신은 송신자가 값을 줄 때까지 블록됩니다. 두 고루틴이 그 지점에서 만나므로, 버퍼 없는 채널은 데이터 파이프이면서 동기화 도구이기도 합니다. 아래에서 <-done이 반환되면 워커가 송신 전에 한 모든 일이 끝났다는 것을 알 수 있습니다:
여기서는 main에서 뮤텍스 없이 result를 읽어도 안전합니다. Go 메모리 모델은 워커가 송신 전에 한 모든 일이 대응하는 수신 후의 main에서 보인다고 보장합니다. 순수한 신호에는 struct{}가 메모리를 차지하지 않으므로 chan struct{}가 관용적인 타입입니다.
버퍼 있는 채널
make(chan T, n)은 채널에 값 n개를 담을 공간을 줍니다. 송신은 버퍼가 가득 찰 때까지 수신자 없이도 성공하고, 수신은 버퍼가 빌 때까지 성공합니다. 값은 들어간 순서대로 나옵니다.
버퍼는 송신자와 수신자를 분리해서 짧은 폭주가 송신자를 멈추지 않게 합니다. 소비자보다 항상 빠른 생산자를 고쳐 주지는 않으며, 송신자가 블록되는 순간을 늦출 뿐입니다. 버퍼 크기는 교착 상태를 없애려고 정하지 말고 이유(송신자 수, 알려진 배치 크기)가 있을 때 정하세요.
len(ch)는 스냅숏입니다. 그 값으로 행동할 즈음에는 다른 고루틴이 바꿨을 수 있으므로, 송신이 블록될지 판단하는 데 쓰지 마세요. 그런 용도에는 default case가 있는 select를 쓰세요.
close와 range
close(ch)는 더 이상 값이 전송되지 않는다고 수신자에게 알립니다. 닫은 뒤에는:
- 버퍼에 이미 있는 값은 여전히 전달되고,
- 그다음에는 모든 수신이 즉시 제로 값을 반환하며,
v, ok := <-ch는ok == false를 보고하고,for v := range ch는 끝납니다.
close 후 첫 번째 수신은 여전히 ok == true와 함께 "last"를 받습니다. 두 번째는 제로 값 ""와 false를 받습니다.
패닉을 일으키는 규칙:
- 닫힌 채널에 보내면
send on closed channel로 패닉이 나고, - 이미 닫힌 채널을 닫으면 패닉이 나며,
nil채널을 닫으면 패닉이 납니다.
그래서 보내는 쪽만, 그리고 한 번만 닫습니다. 송신자가 여럿이면 어느 송신자도 다른 송신자가 언제 끝나는지 모릅니다. 별도의 고루틴이 (sync.WaitGroup으로) 모든 송신자를 기다렸다가 Wait가 반환된 뒤 채널을 닫게 하세요. 닫기는 수신자가 끝을 기다릴 때만 필요합니다. 아무도 참조하지 않는 닫히지 않은 채널은 다른 값처럼 가비지 컬렉션됩니다.
방향 타입
함수는 채널에서 보내기만 하거나 받기만 한다고 선언할 수 있습니다. 그러면 컴파일러가 반대 연산을 거부합니다.
| 타입 | 의미 | 허용되는 것 |
|---|---|---|
chan T | 양방향 | 송신, 수신, 닫기 |
chan<- T | 송신 전용 | 송신, 닫기 |
<-chan T | 수신 전용 | 수신 |
chan T는 함수에 넘길 때 어느 제한된 타입으로든 암묵적으로 변환됩니다. 그래서 위의 produce가 <-chan int를 반환할 수 있습니다. 호출자는 그것을 순회할 수 있지만 거기에 보내거나 닫을 수는 없습니다. 해당하는 모든 함수 매개변수에 방향 타입을 쓰세요. 소유권을 문서화하고 오용을 컴파일 오류로 바꿔 줍니다.
교착 상태
모든 고루틴이 블록되어 아무것도 그중 하나를 깨울 수 없으면 런타임이 프로그램을 중단합니다:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
고루틴 덤프는 어떤 연산이 멈췄는지 알려 줍니다(여기서는 chan send, 그 밖에 chan receive, sync.WaitGroup.Wait, select). 흔한 원인은 다음과 같습니다:
- 받는 쪽이 실행되지 않는 버퍼 없는 채널에 보냄,
- 절대 닫히지 않는 채널을
range로 순회함, - 0에 도달하지 않는
WaitGroup카운터, - 두 고루틴이 서로를 기다림.
런타임은 모든 고루틴이 멈춘 경우만 감지합니다. 다른 고루틴(HTTP 리스너, 티커)이 살아 있는 서버에서는 같은 버그가 아무것도 죽이지 않고, 블록된 고루틴이 그냥 누수됩니다.
nil 채널
nil 채널에 대한 송신과 수신은 영원히 블록됩니다. 쓸모없어 보이지만 select 안에서는 표준 요령입니다. 채널 변수를 nil로 설정하면 그 case가 꺼집니다. 다음 코드는 두 채널을 병합하면서, 각 채널이 닫히면 그 채널을 더 이상 듣지 않습니다:
nil 대입이 없으면 닫힌 채널은 항상 준비된 상태이므로 반복문이 제로 값을 받으며 헛돕니다.
파이프라인
채널은 파이프라인으로 조합됩니다. 각 단계는 한 채널에서 받아 다음 채널로 보내는 고루틴이며, 입력이 끝나면 출력을 닫습니다.
각 단계는 동시에 실행되고, 모든 단계가 순서를 유지하는 고루틴 하나이므로 출력 순서(1, 16, 81)는 결정적입니다. 닫기가 연쇄적으로 일어납니다. generate가 닫으면 square의 range가 끝나고, 그것이 출력을 닫고, 그렇게 main까지 이어집니다.
이 파이프라인의 약점은 main이 일찍 읽기를 멈추면 단계들이 송신에서 영원히 블록된다는 것입니다. 실제 파이프라인은 context.Context나 done 채널을 받아서 모든 송신 옆에서 select합니다.
채널이냐 뮤텍스냐
채널은 데이터의 소유권을 넘기고 이벤트를 알리는 데 씁니다. 캐시나 카운터처럼 여러 고루틴이 제자리에서 읽고 갱신하는 공유 상태를 보호하는 데는 sync.Mutex가 더 단순합니다. 상태를 소유하고 채널로 요청을 처리하는 고루틴보다 뮤텍스를 담은 구조체가 더 명확한 경우가 많습니다. 코드를 짧게 하고 소유권을 분명하게 하는 쪽을 쓰세요.
빠른 참조
| 연산 | nil 채널 | 열린 채널 | 닫힌 채널 |
|---|---|---|---|
ch <- v | 영원히 블록 | 수신되거나 버퍼에 자리가 생길 때까지 블록 | 패닉 |
<-ch | 영원히 블록 | 값이 생길 때까지 블록 | 버퍼의 값, 그다음 제로 값 |
v, ok := <-ch | 영원히 블록 | ok는 true | 비고 나면 ok는 false |
close(ch) | 패닉 | 닫힘 | 패닉 |
len(ch), cap(ch) | 0, 0 | 버퍼에 든 값 수, 버퍼 크기 | 남은 값 수, 버퍼 크기 |
자주 묻는 질문
Go에서 버퍼 있는 채널과 버퍼 없는 채널의 차이는 무엇인가요?
버퍼 없는 채널(make(chan int))에는 저장 공간이 없습니다. 송신은 다른 고루틴이 받을 때까지 블록되므로, 모든 송신이 곧 전달이자 동기화 지점입니다. 버퍼 있는 채널(make(chan int, 3))은 값을 최대 3개까지 담습니다. 송신은 버퍼가 가득 찼을 때만, 수신은 버퍼가 비었을 때만 블록됩니다.
Go에서 닫힌 채널을 읽으면 어떻게 되나요?
닫힌 채널에서의 수신은 절대 블록되지 않습니다. 먼저 버퍼에 남은 값을 모두 내놓고, 그다음에는 요소 타입의 제로 값을 계속 반환합니다. 차이를 알려면 v, ok := <-ch를 쓰세요. 채널이 닫히고 비면 ok가 false입니다. for v := range ch 반복문은 그 시점에 멈춥니다.
Go에서 채널은 누가 닫아야 하나요?
보내는 쪽이 닫아야 하며, 받는 쪽이 더 이상 값이 오지 않는다는 것을 알아야 할 때만(예: range 반복문을 끝내기 위해) 닫습니다. 닫힌 채널에 보내면 패닉이 나고, 채널을 두 번 닫아도 패닉이 나므로, 받는 쪽이 채널을 닫으면 보내는 쪽과 경쟁 상태가 됩니다. 채널을 해제하려고 닫을 필요는 없습니다. 도달할 수 없는 채널은 어떤 경우든 가비지 컬렉터가 회수합니다.
"fatal error: all goroutines are asleep"는 무슨 뜻인가요?
프로그램의 모든 고루틴이 무엇으로도 완료될 수 없는 채널 연산이나 잠금에서 블록되어, 런타임이 프로그램을 멈춘 것입니다. 가장 흔한 원인은 다른 고루틴이 받지 않는데 main에서 버퍼 없는 채널에 보내는 것, 또는 절대 닫히지 않는 채널을 range로 순회하는 것입니다.