Channel은 goroutine 간 데이터를 주고받는 통신 수단이다. Go의 동시성 철학인 "메모리를 공유하지 말고, 통신으로 메모리를 공유하라"를 실현하는 핵심 메커니즘이다.
이번 편에서는 Channel의 기본 동작부터 buffered/unbuffered 차이, 방향 제한, close 규칙까지 완전히 다룬다.
1. Channel 개념과 생성
Channel은 make 함수로 생성한다. 타입이 지정된 파이프라고 생각하면 된다.
ch := make(chan int) // unbuffered channel (int 타입)
ch := make(chan string, 5) // buffered channel (string 타입, 버퍼 크기 5)
2. Send / Receive 동작
Channel에 값을 보내려면 <- 연산자를 사용한다.
ch <- 42 // send: channel에 42를 보냄
val := <-ch // receive: channel에서 값을 받음
func TestChannelSendReceive(t *testing.T) {
ch := make(chan int)
go func() {
ch <- 42 // send
}()
value := <-ch // receive (send될 때까지 blocking)
assert.Equal(t, 42, value)
}
Channel은 다양한 타입을 지원한다. struct를 전달하면 결과와 에러를 함께 보낼 수 있다.
func TestChannelStructType(t *testing.T) {
type Result struct {
Value int
Err error
}
ch := make(chan Result)
go func() {
ch <- Result{Value: 100, Err: nil}
}()
result := <-ch
assert.Equal(t, 100, result.Value)
assert.NoError(t, result.Err)
}
3. Blocking 동작 이해
Channel의 가장 중요한 특성은 blocking이다.
- Unbuffered channel: send와 receive가 동시에 준비되어야 진행된다
- Buffered channel: 버퍼가 가득 차면 send가 blocking, 버퍼가 비면 receive가 blocking
graph LR
subgraph "Unbuffered Channel"
GA["Goroutine A (send - blocking)"] -- "핸드셰이크 (동시에 진행)" --> GB["Goroutine B (receive - blocking)"]
end
graph LR
subgraph "Buffered Channel (크기 3)"
GA2["Goroutine A (send x3 가능, 4번째 send BLOCK!)"] --> BUF["Buffer (크기: 3)"] --> GB2["Goroutine B (receive)"]
end
4. Unbuffered vs Buffered Channel
4.1 Unbuffered Channel
ch := make(chan int) // 버퍼 크기 0
- send하면 receiver가 receive할 때까지 blocking
- 동기적(synchronous) 통신: 양쪽이 모두 준비되어야 진행
func TestUnbufferedChannel(t *testing.T) {
ch := make(chan int)
go func() {
ch <- 42 // receiver가 준비될 때까지 여기서 blocking
}()
time.Sleep(100 * time.Millisecond) // sender는 이미 blocking 중
value := <-ch // receive하는 순간 sender도 진행
assert.Equal(t, 42, value)
}
4.2 Buffered Channel
ch := make(chan int, 3) // 버퍼 크기 3
- 버퍼에 빈 공간이 있으면 send가 즉시 완료
- 버퍼가 가득 차면 send가 blocking
- 비동기적(asynchronous) 통신
func TestBufferedChannel(t *testing.T) {
ch := make(chan int, 3)
// receiver 없이도 3개까지 send 가능
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 → blocking (버퍼 가득 참)
assert.Equal(t, 1, <-ch)
assert.Equal(t, 2, <-ch)
assert.Equal(t, 3, <-ch)
}
4.3 cap과 len
func TestBufferedChannelCapLen(t *testing.T) {
ch := make(chan string, 5)
assert.Equal(t, 5, cap(ch)) // 버퍼 용량
assert.Equal(t, 0, len(ch)) // 현재 대기 중인 값 수
ch <- "a"
ch <- "b"
assert.Equal(t, 5, cap(ch))
assert.Equal(t, 2, len(ch))
}
4.4 언제 어떤 것을 쓸까?
| 상황 | 선택 |
|---|---|
| goroutine 간 동기화(핸드셰이크) 필요 | Unbuffered |
| 신호 전달 (done, quit) | Unbuffered |
| 생산 속도와 소비 속도 차이 완충 | Buffered |
| Producer/Consumer 패턴 | Buffered |
| 성능이 중요한 대량 데이터 전달 | Buffered |
5. Channel 방향 제한
함수 파라미터에서 channel의 방향을 제한할 수 있다.
chan<- int // send-only (보내기만 가능)
<-chan int // receive-only (받기만 가능)
이를 통해 컴파일 타임에 channel의 잘못된 사용을 방지할 수 있다.
// producer: send-only channel
func produce(ch chan<- int, values []int) {
for _, v := range values {
ch <- v
}
close(ch)
}
// consumer: receive-only channel
func consume(ch <-chan int) []int {
var results []int
for v := range ch {
results = append(results, v)
}
return results
}
func TestChannelDirection(t *testing.T) {
ch := make(chan int, 5)
go produce(ch, []int{1, 2, 3, 4, 5})
results := consume(ch)
assert.Equal(t, []int{1, 2, 3, 4, 5}, results)
}
양방향 channel은 send-only 또는 receive-only로 암묵적 변환된다. 반대 방향으로의 변환은 컴파일 에러가 발생한다.
var sendOnly chan<- int = ch // OK: 양방향 → send-only
var recvOnly <-chan int = ch // OK: 양방향 → receive-only
6. Channel Close
6.1 close의 의미
close(ch)는 "이 channel에 더 이상 값을 보내지 않겠다"는 선언이다.
6.2 닫힌 Channel에서 Receive
func TestReceiveFromClosedChannel(t *testing.T) {
ch := make(chan int, 1)
ch <- 42
close(ch)
// 버퍼에 값이 있으면 정상 반환
val, ok := <-ch
assert.Equal(t, 42, val)
assert.True(t, ok) // 유효한 값
// 버퍼가 비고 닫힌 channel → zero value + false
val, ok = <-ch
assert.Equal(t, 0, val) // int의 zero value
assert.False(t, ok) // 닫힘 표시
}
6.3 Close 규칙
| 규칙 | 설명 |
|---|---|
| Sender가 close | receiver가 아닌 sender가 close 책임 |
| 두 번 close 금지 | 이미 닫힌 channel을 다시 닫으면 panic |
| 닫힌 channel에 send 금지 | 닫힌 channel에 값을 보내면 panic |
| 닫힌 channel에서 receive 가능 | zero value + false 반환 |
6.4 Close 책임 패턴
// 패턴: sender가 channel을 생성하고, 보내고, close한다
func generator() <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // sender가 close 책임
for i := range 5 {
ch <- i
}
}()
return ch // receive-only로 반환
}
7. Range over Channel
range를 사용하면 channel이 닫힐 때까지 자동으로 값을 수신한다.
func TestRangeOverChannel(t *testing.T) {
ch := make(chan int, 5)
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // range가 종료되려면 close 필수!
}()
var results []int
for v := range ch {
results = append(results, v)
}
assert.Equal(t, []int{1, 2, 3, 4, 5}, results)
}
range ch가 종료되려면 반드시close(ch)가 호출되어야 한다. close하지 않으면 range는 영원히 blocking된다.
7.1 신호용 Channel
데이터 전달이 아닌 완료 신호만 보내려면 chan struct{}를 사용한다. struct{}는 메모리를 차지하지 않는다.
func TestChannelSignaling(t *testing.T) {
done := make(chan struct{})
go func() {
// 작업 수행...
close(done) // 완료 신호 (모든 receiver에게 전달)
}()
<-done // 완료 대기
}
close(done)은 모든 receiver에게 동시에 신호를 보낸다. 이것이 단순히 done <- struct{}{}와의 차이점이다.
struct{}와struct{}{}의 차이가 헷갈린다면 FAQ를 참고하자.
8. 실습: Producer / Consumer 패턴
func TestProducerConsumer(t *testing.T) {
ch := make(chan int, 10)
var results []int
// Producer: 제곱수 생성
go func() {
for i := 1; i <= 10; i++ {
ch <- i * i
}
close(ch)
}()
// Consumer: 결과 수집
for val := range ch {
results = append(results, val)
}
expected := []int{1, 4, 9, 16, 25, 36, 49, 64, 81, 100}
assert.Equal(t, expected, results)
}
8.1 여러 Producer
func TestMultipleProducers(t *testing.T) {
ch := make(chan int, 20)
var wg sync.WaitGroup
// 3개의 producer
for p := range 3 {
wg.Add(1)
go func() {
defer wg.Done()
for i := range 5 {
ch <- p*100 + i
}
}()
}
// producer가 모두 끝나면 channel close
go func() {
wg.Wait()
close(ch)
}()
var results []int
for val := range ch {
results = append(results, val)
}
assert.Len(t, results, 15) // 3 x 5 = 15개
}
9. 퀴즈
여기까지 읽었으면 풀 수 있는 문제들이다. 답을 고르면 바로 해설이 나온다.
- type: mcq
q: "Unbuffered channel과 Buffered channel의 차이를 바르게 설명한 것은?"
choices: ["Buffered는 receiver 없이도 버퍼 크기만큼 send가 된다", "Unbuffered는 버퍼가 하나라도 있어야 send가 가능하다", "Buffered는 버퍼가 가득 차도 send가 blocking되지 않는다", "Unbuffered는 send한 뒤 receiver를 기다리지 않는다"]
answer: 0
explain: "Buffered channel은 버퍼에 빈 공간이 있으면 send가 즉시 완료되므로 receiver가 없어도 버퍼 크기만큼 보낼 수 있다. 버퍼가 가득 차면 그때부터 send가 blocking된다. 반면 unbuffered channel은 버퍼 크기가 0이라 receiver가 receive할 때까지 send가 blocking되는 동기적 통신이다. (4.1절, 4.2절)"
- type: code
q: "이 코드를 그대로 실행하면 (1)에서 어떤 일이 벌어지나?"
lang: go
code: |
ch := make(chan int)
ch <- 42 // (1)
value := <-ch // (2)
_ = value
choices: ["값이 내부 버퍼에 저장되고 다음 줄로 넘어간다", "unbuffered라 send 자체가 컴파일 에러가 된다", "위의 make가 버퍼를 1로 잡아 주어 통과한다", "receiver가 없어 blocking되어 더 진행되지 않는다"]
answer: 3
explain: "make(chan int)는 버퍼 크기가 0인 unbuffered channel이다. unbuffered channel은 send와 receive가 동시에 준비되어야 진행되는데, 여기서는 receive하는 (2)가 같은 goroutine의 다음 줄에 있어 실행될 기회가 없다. 그래서 (1)에서 멈춘다. (3장, 4.1절)"
- type: ox
q: "close(done)은 대기 중인 모든 receiver에게 동시에 완료 신호를 전달한다."
answer: true
explain: "맞다. close(done)은 모든 receiver에게 동시에 신호를 보내며, 이것이 receiver 하나에게 값을 하나만 넘기는 일반 send와의 차이점이다. 그래서 완료 신호를 여러 곳에 뿌릴 때 close를 쓴다. (7.1절)"
- type: blank
q: "버퍼 크기 5로 만든 channel에 값을 2개 넣어도 그대로 5를 돌려주는 내장 함수는 ___(ch)이다."
answer: ["cap"]
explain: "cap(ch)다. cap은 make에 지정한 버퍼 용량이라 값이 들어오고 나가도 변하지 않는다. 지금 버퍼에 대기 중인 값의 개수는 len(ch)로 따로 확인하며, 버퍼 크기 5인 channel에 값 2개를 넣으면 cap은 5, len은 2다. (4.3절)"
- type: mcq
q: "Channel의 close 규칙으로 옳은 것은?"
choices: ["close 책임은 sender가 아니라 receiver에게 있다", "이미 닫힌 channel은 다시 닫아도 아무 일이 없다", "닫힌 channel에 값을 보내면 panic이 발생한다", "닫힌 channel에서 값을 받으면 panic이 발생한다"]
answer: 2
explain: "닫힌 channel에 send하면 panic이 발생한다. close 책임은 값을 보내는 sender에게 있고, 이미 닫힌 channel을 다시 닫아도 panic이다. 반대로 닫힌 channel에서 receive하는 것은 허용되며 zero value와 false를 돌려준다. (6.3절)"
- type: code
q: "이 코드에서 v2와 ok2에 담기는 값은?"
lang: go
code: |
ch := make(chan int, 1)
ch <- 42
close(ch)
v1, ok1 := <-ch
v2, ok2 := <-ch
_, _, _, _ = v1, ok1, v2, ok2
choices: ["v2는 42이고 ok2는 true로 그대로 나온다", "v2는 0이고 ok2는 false로 나온다", "두 번째 receive에서 panic이 발생한다", "두 번째 receive가 그 자리에서 blocking된다"]
answer: 1
explain: "첫 번째 receive는 버퍼에 남아 있던 42와 true를 가져간다. 그 다음에는 버퍼가 비고 channel도 닫힌 상태이므로 두 번째 receive는 blocking되지 않고 곧바로 int의 zero value인 0과 닫힘을 뜻하는 false를 반환한다. (6.2절)"
- type: ox
q: "sender가 보낼 값을 모두 보내고 나면 range ch는 close 없이도 자동으로 종료된다."
answer: false
explain: "아니다. range ch가 종료되려면 반드시 close(ch)가 호출되어야 한다. 보낼 값을 다 보냈더라도 close하지 않으면 range는 다음 값을 기다리며 영원히 blocking된다. (7장)"
- type: blank
q: "데이터 없이 완료 신호만 주고받을 때는 메모리를 0바이트 차지하는 chan ___{} 타입을 쓴다."
answer: ["struct", "struct{}"]
explain: "chan struct{}이다. struct{}는 필드가 없는 빈 구조체 타입이라 메모리를 0바이트 차지하므로, 데이터 없이 신호만 전달할 때 가장 효율적인 선택이다. 값을 보낼 때는 인스턴스인 struct{}{}를 쓴다. (7.1절)"
- type: mcq
q: "본문의 선택 기준에서 Unbuffered channel이 권장되는 상황은?"
choices: ["goroutine 사이의 핸드셰이크 동기화가 필요할 때", "생산 속도와 소비 속도의 차이를 완충하고 싶을 때", "Producer/Consumer 패턴으로 처리량을 낼 때", "성능이 중요한 대량 데이터를 전달해야 할 때"]
answer: 0
explain: "goroutine 간 동기화(핸드셰이크)와 done, quit 같은 신호 전달에는 unbuffered를 쓴다. 생산·소비 속도 차이 완충, Producer/Consumer 패턴, 성능이 중요한 대량 데이터 전달은 모두 buffered 쪽이다. (4.4절)"
- type: code
q: "이 코드에서 컴파일 에러가 나는 줄은?"
lang: go
code: |
ch := make(chan int, 5)
var sendOnly chan<- int = ch // (1)
var recvOnly <-chan int = ch // (2)
var both chan int = recvOnly // (3)
_, _, _ = sendOnly, recvOnly, both
choices: ["(1) — 양방향 channel은 send-only가 될 수 없다", "(2) — 양방향 channel은 receive-only가 못 된다", "(3)까지 포함해 세 줄 모두 정상 컴파일된다", "(3) — 방향이 제한된 channel은 양방향이 못 된다"]
answer: 3
explain: "양방향 channel은 send-only나 receive-only로 암묵적 변환되므로 (1)과 (2)는 정상이다. 하지만 반대 방향, 즉 방향이 제한된 channel을 다시 양방향으로 돌리는 (3)은 컴파일 에러다. (5장)"
10. 정리
| 개념 | 핵심 |
|---|---|
| Channel | goroutine 간 타입 안전한 통신 수단 |
| Unbuffered | 동기적 핸드셰이크, send/receive 동시 준비 필요 |
| Buffered | 비동기적 큐, 버퍼 크기만큼 send 가능 |
| 방향 제한 | chan<- (send-only), <-chan (receive-only) |
| Close | sender가 책임, 닫힌 channel은 zero value 반환 |
| Range | channel 닫힐 때까지 자동 수신 |
chan struct{} | 신호 전달용 (메모리 0) |
다음 편에서는 여러 channel을 동시에 처리하는 select문과 fan-in/fan-out 등 channel 심화 패턴을 다룬다.
11. FAQ
11.1 Q. struct{}와 struct{}{}의 차이는?
struct{}는 타입이고, struct{}{}는 **값(인스턴스)**이다.
일반 구조체로 비유하면 이해하기 쉽다:
type Person struct {
Name string
Age int
}
p := Person{Name: "Frank", Age: 30}
// ^^^^^^ 타입
// ^^^^^^^^^^^^^^^^^^^^^^^^ 값
마찬가지로 빈 구조체도 동일한 구조다:
v := struct{}{}
// ^^^^^^^^ 타입: struct{} (필드가 없는 구조체)
// ^^ 값: {} (빈 리터럴)
| 표현 | 의미 | 비유 |
|---|---|---|
int | int 타입 | 설계도 |
42 | int 값 | 실물 |
struct{} | 빈 구조체 타입 | 설계도 (필드 없음) |
struct{}{} | 빈 구조체 값 | 실물 (내용 없음) |
Channel에서 사용할 때:
// channel 생성 — 타입을 지정
done := make(chan struct{}) // struct{} 타입의 channel
// 값을 보냄 — struct{}{} 인스턴스를 send
done <- struct{}{}
// close — 값 대신 "닫힘" 신호를 모든 receiver에게 전달
close(done)
struct{}는 메모리를 0바이트 차지하므로, 데이터 없이 신호만 전달할 때 가장 효율적인 선택이다.