
고랭(Golang) 채널 마스터하기: 기초부터 전문가까지
채널(Channel)은 고(Go) 언어의 핵심 타입(type) 중 하나입니다.
마치 파이프라인(pipeline)처럼 생각할 수 있으며, 이를 통해 동시에 실행되는 고루틴(goroutine)들이 데이터를 주고받으며 통신할 수 있습니다.
채널(Channel)의 연산자(operator)는 화살표 <-
입니다.
채널 연산 예시
ch <- v
: 값v
를 채널ch
로 보냅니다(Send).v := <-ch
: 채널ch
로부터 데이터를 받아 변수v
에 할당합니다(Receive).
(화살표의 방향이 데이터 흐름의 방향을 나타냅니다.
)
채널 생성 및 사용
맵(map)이나 슬라이스(slice) 같은 데이터 타입과 마찬가지로, 채널(channel)도 사용하기 전에 반드시 생성해야 합니다.
ch := make(chan int) // int 타입 데이터를 주고받는 채널 생성
채널 타입 (Channel Types)
채널(Channel) 타입의 정의 형식은 다음과 같습니다.
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType.
세 가지 유형의 정의가 있으며, 선택적인 <-
는 채널(channel)의 방향을 나타냅니다.
만약 방향이 지정되지 않으면, 채널(channel)은 양방향(bidirectional)이며 데이터를 받거나 보낼 수 있습니다.
chan T
: 타입T
의 데이터를 받거나 보낼 수 있습니다.chan<- float64
: 타입float64
의 데이터만 보낼 수 있습니다(Send-only).<-chan int
: 타입int
의 데이터만 받을 수 있습니다(Receive-only).
<-
는 항상 가장 왼쪽에 있는 타입과 먼저 결합합니다.
예를 들면 다음과 같습니다.
chan<- chan int
:chan<- (chan int)
와 동일합니다.
(int 채널을 보내기만 하는 채널)chan<- <-chan int
:chan<- (<-chan int)
와 동일합니다.
(int를 받기만 하는 채널을 보내기만 하는 채널)<-chan <-chan int
:<-chan (<-chan int)
와 동일합니다.
(int를 받기만 하는 채널을 받기만 하는 채널)chan (<-chan int)
: int를 받기만 하는 채널을 주고받을 수 있는 채널
로 채널 초기화 및 용량 설정
make(chan int, 100) // 용량(capacity)이 100인 int 채널 생성
용량(capacity)은 채널(Channel)이 최대로 보유할 수 있는 요소의 수, 즉 채널(Channel)의 버퍼(buffer) 크기를 나타냅니다.
만약 용량(capacity)이 설정되지 않거나 0으로 설정되면, 채널(Channel)은 버퍼(buffer)가 없는 것(unbuffered channel)을 의미하며, 보내는 쪽(sender)과 받는 쪽(receiver)이 모두 준비되었을 때만 통신이 이루어집니다 (이때 블로킹(Blocking) 발생).
버퍼(buffer)를 설정하면, 블로킹(blocking)이 발생하지 않을 수 있습니다.
오직 버퍼(buffer)가 가득 찼을 때만 보내기(send) 작업이 블로킹(block)되고, 버퍼(buffer)가 비어 있을 때만 받기(receive) 작업이 블로킹(block)됩니다.nil
채널(channel)과는 통신할 수 없습니다.
(영원히 블로킹됨)
채널 닫기 (Closing Channel)
채널(Channel)은 내장 함수(built-in function) close
를 통해 닫을 수 있습니다.
여러 고루틴(goroutine)이 추가적인 동기화 조치 없이 채널(channel)로부터 데이터를 받거나 보낼 수 있습니다.
채널(Channel)은 선입선출(First-In-First-Out, FIFO) 큐(queue)처럼 동작할 수 있으며, 데이터를 보내고 받는 순서는 일관성이 있습니다.
채널 수신 시 다중 값 할당 지원
v, ok := <-ch
이 방식은 채널(Channel)이 닫혔는지 여부를 확인하는 데 사용될 수 있습니다.ok
값이 false
이면 채널(channel)이 닫혔고 더 이상 보낼 데이터가 없음을 의미합니다.
보내기 구문 (Send Statement)
보내기(send) 구문은 채널(Channel)에 데이터를 보내는 데 사용됩니다.
(예: ch <- 3
).
정의는 다음과 같습니다.
SendStmt = Channel "<-" Expression.
Channel = Expression.
통신이 시작되기 전에, 채널(channel)과 표현식(Expression)이 모두 평가되어야 합니다.
예를 들면 다음과 같습니다.
package main
import "fmt"
func main() {
c := make(chan int) // 버퍼 없는 채널 생성
defer close(c) // main 함수 종료 시 채널 닫기
go func() {
// 다른 고루틴에서 채널에 값을 보냄
c <- 3 + 4 // (3 + 4)가 먼저 계산된 후 7이 보내짐
}()
// 메인 고루틴에서 채널로부터 값을 받음
i := <-c
fmt.Println(i) // 출력: 7
}
위 코드에서, (3 + 4)
가 먼저 7로 계산된 다음 채널(channel)로 보내집니다.
보내기(send)가 실행될 때까지 통신은 블로킹(block)됩니다.
앞서 언급했듯이, 버퍼(buffer) 없는 채널(unbuffered channel)의 경우, 받는 쪽(receiver)이 준비되었을 때만 보내기(send) 작업이 실행됩니다.
만약 버퍼(buffer)가 있고 버퍼(buffer)가 가득 차지 않았다면, 보내기(send) 작업은 즉시 실행됩니다.
닫힌 채널(closed channel)에 계속 데이터를 보내려고 하면 런타임 패닉(run-time panic)이 발생합니다.nil
채널(channel)에 데이터를 보내려고 하면 영원히 블로킹(block)됩니다.
수신 연산자 (Receive Operator)
<-ch
는 채널 ch
로부터 데이터를 받는 데 사용됩니다.
이 표현식은 받을 데이터가 있을 때까지 블로킹(block)됩니다.nil
채널(channel)로부터 데이터를 받으려고 하면 영원히 블로킹(block)됩니다.
닫힌 채널(closed channel)로부터 데이터를 받는 것은 블로킹(block)되지 않고 즉시 반환됩니다.
채널(channel)을 통해 보내진 데이터를 모두 받은 후에는, 해당 요소 타입(element type)의 제로 값(zero value)을 계속 반환합니다.
앞서 언급했듯이, 추가 반환 파라미터(parameter)를 사용하여 채널(channel)이 닫혔는지 확인할 수 있습니다.
x, ok := <-ch // ok가 true이면 정상 수신, false이면 채널이 닫혔거나 비어있음 (이후 제로 값 반환)
x, ok = <-ch
var x, ok = <-ch
만약 ok
가 false
라면, 수신된 x
는 생성된 제로 값(zero value)이며 채널(channel)이 닫혔거나 비어있음을 나타냅니다.
블로킹 (Blocking)
기본적으로, 보내기(send)와 받기(receive)는 상대방이 준비될 때까지 블로킹(block)됩니다.
이 방식은 명시적인 락(lock)이나 조건 변수(conditional variable)를 사용하지 않고도 고루틴(goroutine) 간의 동기화(synchronization)에 사용될 수 있습니다.
예를 들어, 공식 예제는 다음과 같습니다.
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 계산된 합계를 채널 c로 보냄
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // 결과를 받을 채널 생성
// 슬라이스를 반으로 나누어 각각의 합계를 고루틴에서 계산
go sum(s[:len(s)/2], c) // 앞부분 합계 계산
go sum(s[len(s)/2:], c) // 뒷부분 합계 계산
// 채널 c로부터 두 개의 결과값을 받음
// 두 고루틴이 모두 값을 보낼 때까지 여기서 블로킹됨
x, y := <-c, <-c
fmt.Println(x, y, x+y) // 결과 출력
}
위 코드에서, x, y := <-c, <-c
구문은 두 개의 sum
고루틴(goroutine)이 계산 결과를 채널(channel)로 보낼 때까지 계속 기다립니다(block).
버퍼 채널 (Buffered Channels)
make
함수의 두 번째 파라미터(parameter)는 버퍼(buffer)의 크기를 지정합니다.
ch := make(chan int, 100) // 크기가 100인 버퍼 채널 생성
버퍼(buffer)를 사용함으로써, 블로킹(blocking)을 최대한 피하고 애플리케이션(application) 성능을 향상시킬 수 있습니다.
버퍼(buffer)가 가득 차지 않은 한 보내기(send)는 즉시 반환되고, 버퍼(buffer)가 비어 있지 않은 한 받기(receive)는 즉시 반환됩니다.
Range
for ... range
구문은 채널(channel)을 처리할 수 있습니다.
package main
import (
"fmt"
"time"
)
func main() {
// 이 고루틴은 메인 함수가 종료되는 것을 막기 위해 존재 (여기서는 큰 의미 없음)
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int) // 채널 생성
// 다른 고루틴에서 채널에 0부터 9까지 값을 보내고 채널을 닫음
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c) // 채널 닫기! 중요!
}()
// range를 사용하여 채널 c로부터 값을 계속 받음
for i := range c {
fmt.Println(i) // 받은 값을 출력
}
// 채널 c가 닫히면 for 루프가 종료되고 여기가 실행됨
fmt.Println("Finished")
}
range c
에 의해 생성되는 반복 값은 채널(Channel)에서 보내진 값입니다.
채널(channel)이 닫힐 때까지 계속 반복합니다.
위 예제에서 만약 close(c)
가 주석 처리된다면, 프로그램은 for ... range
라인에서 영원히 블로킹(block)될 것입니다 (데드락 발생).
select
select
구문은 여러 개의 가능한 보내기(send) 및 받기(receive) 작업 집합 중에서 하나를 선택하여 처리하는 데 사용됩니다.switch
와 유사하지만, 오직 통신 작업만을 처리하는 데 사용됩니다.case
는 보내기 구문, 받기 구문, 또는 default
가 될 수 있습니다.
받기 구문은 하나 또는 두 개의 변수에 값을 할당할 수 있으며, 반드시 받기 작업이어야 합니다.
최대 하나의 default
케이스(case)만 허용되며, 보통 case
목록의 마지막에 위치합니다.
예를 들면 다음과 같습니다.
package main
import "fmt"
// 피보나치 수열을 생성하여 채널 c로 보내고, quit 채널로부터 신호를 받으면 종료
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // c 채널로 현재 값 x를 보낼 수 있으면 보냄
x, y = y, x+y // 다음 피보나치 수 계산
case <-quit: // quit 채널로부터 값을 받을 수 있으면 받음 (종료 신호)
fmt.Println("quit")
return // 함수 종료
}
}
}
func main() {
c := make(chan int) // 피보나치 수를 받을 채널
quit := make(chan int) // 종료 신호를 보낼 채널
// 다른 고루틴에서 피보나치 수 10개를 채널 c로부터 받아 출력
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c) // 채널 c로부터 값 수신 및 출력
}
// 10개를 모두 받으면 quit 채널에 0을 보내 종료 신호 전달
quit <- 0
}()
// 메인 고루틴에서 피보나치 함수 실행
fibonacci(c, quit)
}
만약 동시에 여러 case
가 처리될 수 있다면 (예: 여러 채널(channel)에서 동시에 데이터를 받을 수 있는 경우), 고(Go)는 의사 난수(pseudo-random) 방식으로 처리할 case
하나를 선택합니다.
만약 처리해야 할 case
가 없다면, default
가 선택되어 처리됩니다 (default
케이스(case)가 존재하는 경우).
만약 default
케이스(case)가 없다면, select
구문은 처리해야 할 case
가 생길 때까지 블로킹(block)됩니다.nil
채널(channel)에 대한 연산은 영원히 블로킹(block)된다는 점에 유의하세요.
만약 default
케이스(case)가 없고 오직 nil
채널(channel)만 있는 select
는 영원히 블로킹(block)됩니다.select
구문은 switch
구문과 마찬가지로 루프(loop)가 아니며, 오직 하나의 case
만 선택하여 처리합니다.
만약 채널(channel)을 계속해서 처리하고 싶다면, 외부에 무한 for
루프(loop)를 추가할 수 있습니다.
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
타임아웃 (timeout)
select
의 중요한 응용 중 하나는 타임아웃(timeout) 처리입니다.select
구문은 처리할 case
가 없으면 블로킹(block)되므로, 이때 타임아웃(timeout) 작업이 필요할 수 있습니다.
예를 들면 다음과 같습니다.
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1) // 버퍼 크기 1인 문자열 채널
// 2초 후에 채널 c1에 데이터를 보내는 고루틴
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
// select 문으로 c1 채널 수신 또는 타임아웃 대기
select {
case res := <-c1: // c1으로부터 데이터를 받으면 실행
fmt.Println(res)
case <-time.After(time.Second * 1): // 1초 후에 타임아웃 채널로부터 신호를 받으면 실행
fmt.Println("timeout 1")
}
}
위 예제에서는 2초 후에 채널 c1
에 데이터가 보내지지만, select
는 1초 후에 타임아웃(timeout)되도록 설정되어 있습니다.
따라서 "result 1" 대신 "timeout 1"이 출력됩니다.
이 예제는 time.After
메서드(method)를 사용하는데, 이 메서드(method)는 <-chan Time
타입의 단방향 채널(channel)을 반환합니다.
지정된 시간이 지나면, 현재 시간이 반환된 채널(channel)로 보내집니다.
타이머(Timer)와 티커(Ticker)
타이머 (Timer)
미래의 단일 이벤트를 나타내는 타이머(timer)입니다.
대기 시간을 지정할 수 있으며, 채널(Channel)을 제공합니다.
지정된 시간이 되면, 해당 채널(Channel)은 시간 값을 제공합니다.
예를 들면 다음과 같습니다.
package main
import (
"fmt"
"time"
)
func main() {
timer1 := time.NewTimer(time.Second * 2) // 2초 후에 이벤트 발생하는 타이머 생성
// timer1.C 채널에서 시간 값을 받을 때까지 블로킹됨 (약 2초)
<-timer1.C
fmt.Println("Timer 1 expired") // 2초 후 출력됨
}
위 코드의 두 번째 라인은 약 2초 동안 블로킹(block)되었다가 시간이 되면 계속 실행됩니다.
물론 단순히 기다리기만 하려면 time.Sleep
을 사용할 수도 있습니다.timer.Stop
을 사용하여 타이머(timer)를 중지할 수도 있습니다.
package main
import (
"fmt"
"time"
)
func main() {
timer2 := time.NewTimer(time.Second) // 1초 타이머 생성
// 고루틴에서 타이머 만료를 기다림
go func() {
<-timer2.C
fmt.Println("Timer 2 expired") // Stop()에 의해 중지되면 실행되지 않음
}()
// 타이머 중지 시도
stop2 := timer2.Stop()
if stop2 {
// Stop()이 성공적으로 타이머를 중지시켰다면 true 반환
fmt.Println("Timer 2 stopped")
}
// 메인 고루틴이 바로 종료되지 않도록 잠시 대기 (예시용)
time.Sleep(2 * time.Second)
}
티커 (Ticker)
정기적으로 이벤트를 발생시키는 타이머(timer)입니다.
일정한 간격(interval)으로 채널(Channel)에 이벤트(현재 시간)를 보냅니다.
채널(Channel)의 수신자(receiver)는 고정된 시간 간격으로 채널(Channel)로부터 이벤트를 읽을 수 있습니다.
예를 들면 다음과 같습니다.
package main
import (
"fmt"
"time"
)
func main() {
// 500밀리초 간격으로 이벤트를 발생시키는 티커 생성
ticker := time.NewTicker(time.Millisecond * 500)
done := make(chan bool) // 종료 신호를 위한 채널
// 고루틴에서 티커 이벤트를 받아 처리
go func() {
for {
select {
case <-done: // 종료 신호를 받으면 루프 종료
return
case t := <-ticker.C: // 티커 채널로부터 시간 값을 받으면 실행
fmt.Println("Tick at", t)
}
}
}()
// 메인 고루틴에서 1600밀리초 동안 대기 (약 3번의 Tick 발생)
time.Sleep(1600 * time.Millisecond)
ticker.Stop() // 티커 중지
done <- true // 고루틴에 종료 신호 전송
fmt.Println("Ticker stopped")
}
타이머(timer)와 유사하게, 티커(ticker)도 Stop
메서드(method)를 통해 중지될 수 있습니다.
일단 중지되면, 수신자(receiver)는 더 이상 해당 채널(channel)로부터 데이터를 받지 못합니다.
close
내장 함수(built-in function) close
는 채널(channel)을 닫는 데 사용될 수 있습니다.
채널(channel)이 닫힌 후 보내는 쪽(sender)과 받는 쪽(receiver)의 동작을 요약하면 다음과 같습니다.
- 만약 채널
c
가 닫혔다면, 그 채널(channel)에 계속 데이터를 보내려고 하면 패닉(panic)이 발생합니다:send on closed channel
.
예를 들면 다음과 같습니다. package main import "time" func main() { // 이 고루틴은 메인 함수 종료 방지용 (큰 의미 없음) go func() { time.Sleep(time.Hour) }() c := make(chan int, 10) c <- 1 c <- 2 close(c) // 채널 닫기 c <- 3 // 닫힌 채널에 보내려고 하면 패닉 발생! }
- 닫힌 채널(channel)로부터는 이미 보내진 데이터를 읽을 수 있을 뿐만 아니라, 데이터가 소진된 후에는 해당 타입의 제로 값(zero value)을 계속해서 읽을 수 있습니다.
package main import "fmt" func main() { c := make(chan int, 10) c <- 1 c <- 2 close(c) // 채널 닫기 fmt.Println(<-c) // 1 출력 fmt.Println(<-c) // 2 출력 fmt.Println(<-c) // 0 출력 (int의 제로 값) fmt.Println(<-c) // 0 출력 (int의 제로 값) }
- 만약
range
를 통해 읽는다면,for
루프(loop)는 채널(channel)이 닫힌 후에 자동으로 종료됩니다. package main import "fmt" func main() { c := make(chan int, 10) c <- 1 c <- 2 close(c) // 채널 닫기 // range는 채널이 닫힐 때까지 값을 읽고, 닫히면 루프 종료 for i := range c { fmt.Println(i) // 1, 2 출력 } fmt.Println("Range loop finished") }
i, ok := <-c
를 통해 채널(Channel)의 상태를 확인하고, 읽은 값이 제로 값(zero value)인지 아니면 정상적으로 읽은 값인지 구별할 수 있습니다.package main import "fmt" func main() { c := make(chan int, 10) close(c) // 채널 닫기 i, ok := <-c // 닫힌 채널에서 읽기 시도 // i는 int의 제로 값 0, ok는 false가 됨 fmt.Printf("%d, %t", i, ok) // 출력: 0, false }
동기화 (Synchronization)
채널(Channel)은 고루틴(goroutine) 간의 동기화(synchronization)를 위해 사용될 수 있습니다.
예를 들어, 다음 예제에서는 main
고루틴(goroutine)이 done
채널(channel)을 통해 worker
가 작업을 완료하기를 기다립니다.worker
는 작업을 완료한 후 채널(channel)에 데이터를 보내 main
고루틴(goroutine)에게 작업이 완료되었음을 알릴 수 있습니다.
package main
import (
"fmt"
"time"
)
// 작업을 수행하고 완료되면 done 채널에 true를 보내는 워커 함수
func worker(done chan bool) {
fmt.Println("워커 작업 시작...")
time.Sleep(time.Second) // 작업을 시뮬레이션하기 위해 1초 대기
fmt.Println("워커 작업 완료!")
// 작업이 완료되었음을 알림
done <- true
}
func main() {
done := make(chan bool, 1) // 동기화를 위한 채널 생성 (버퍼 1)
go worker(done) // 워커 고루틴 시작
// 작업이 완료될 때까지 기다림 (done 채널로부터 값을 받을 때까지 블로킹)
fmt.Println("메인: 워커 완료 대기 중...")
<-done
fmt.Println("메인: 워커 완료 신호 받음!")
}
'Go' 카테고리의 다른 글
Go 언어로 JWT 완벽 마스터! 안전한 로그인과 권한 부여, A부터 Z까지 파헤치기 (JWT 초보 탈출 가이드) (0) | 2025.05.07 |
---|---|
고(Go) 1.24 정식 출시! 더 빠르고, 똑똑해지고, 강력해진 Go 언어의 모든 것, 지금부터 파헤쳐 볼까요? (0) | 2025.05.07 |
랭(Golang)에서 로컬 SSH 설정 파일 읽어 원격 서버 접속하기 (0) | 2025.05.06 |
고랭(Go) 슬라이스 전달과 append 함수의 비밀 파헤치기 (0) | 2025.05.06 |
고랭(Go) 빈 문자열 검사 완벽 정복: 쉬운 방법 두 가지 (+공백 처리 팁) (0) | 2025.05.06 |