Golang 채널 마스터링: 제로에서 히로까지
Lukas Schneider
DevOps Engineer · Leapcell

Channel은 Go 언어의 핵심 타입입니다. 이는 동시성 코어 유닛들이 데이터를 송수신하여 통신을 달성할 수 있는 파이프라인으로 간주될 수 있습니다. 이의 연산자는 화살표 <-입니다.
Channel 연산 예시
ch <- v: 값v를 채널ch로 보냅니다.v := <-ch: 채널ch에서 데이터를 수신하고 데이터를v에 할당합니다. (화살표의 방향은 데이터 흐름 방향을 나타냅니다.)
Channel 생성 및 사용
map 및 slice와 같은 데이터 타입과 유사하게, 채널은 사용하기 전에 생성해야 합니다.
ch := make(chan int)
Channel 타입
Channel 타입의 정의 형식은 다음과 같습니다.
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType.
세 가지 타입의 정의를 포함하며, 선택적인 <-는 채널의 방향을 나타냅니다. 방향이 지정되지 않으면 채널은 양방향이며, 데이터를 수신하고 보낼 수 있습니다.
chan T: 타입T의 데이터를 수신하고 보낼 수 있습니다.chan<- float64: 타입float64의 데이터만 보내는 데 사용할 수 있습니다.<-chan int: 타입int의 데이터만 수신하는 데 사용할 수 있습니다.
<-는 항상 가장 왼쪽 타입과 먼저 결합합니다. 예를 들어:
chan<- chan int:chan<- (chan int)와 동일합니다.chan<- <-chan int:chan<- (<-chan int)와 동일합니다.<-chan <-chan int:<-chan (<-chan int)와 동일합니다.chan (<-chan int).
make로 Channel 초기화 및 용량 설정
make(chan int, 100)
용량은 Channel이 보유할 수 있는 최대 요소 수, 즉 Channel 버퍼의 크기를 나타냅니다. 용량이 설정되지 않았거나 0으로 설정된 경우, Channel에 버퍼가 없음을 의미하며, 송신자와 수신자가 모두 준비된 경우에만 통신(Blocking)이 발생합니다. 버퍼를 설정한 후에는 블로킹이 발생하지 않을 수 있습니다. 버퍼가 가득 찼을 때만 send 작업이 블록되고, 버퍼가 비어 있을 때 receive 작업이 블록됩니다. nil 채널은 통신하지 않습니다.
Channel 닫기
Channel은 내장된 close 메서드를 통해 닫을 수 있습니다. 여러 고루틴은 추가적인 동기화 조치를 고려하지 않고도 채널에서 데이터를 수신/전송할 수 있습니다. Channel은 선입선출(FIFO) 큐 역할을 할 수 있으며, 데이터 수신 및 전송 순서가 일치합니다.
Channel의 수신은 다중 - 값 할당을 지원합니다.
v, ok := <-ch
이 방법은 Channel이 닫혔는지 여부를 확인하는 데 사용할 수 있습니다.
send 문
send 문은 데이터를 Channel로 보내는 데 사용됩니다(예: ch <- 3). 이의 정의는 다음과 같습니다.
SendStmt = Channel "<-" Expression. Channel = Expression.
통신이 시작되기 전에 채널과 표현식이 모두 평가되어야 합니다. 예를 들어:
c := make(chan int) defer close(c) go func() { c <- 3 + 4 }() i := <-c fmt.Println(i)
위 코드에서 (3 + 4)는 먼저 7로 계산된 다음 채널로 전송됩니다. 통신은 send가 실행될 때까지 블록됩니다. 앞서 언급했듯이, 버퍼링되지 않은 채널의 경우 수신자가 준비된 경우에만 send 작업이 실행됩니다. 버퍼가 있고 버퍼가 가득 차지 않은 경우 send 작업이 실행됩니다. 닫힌 채널에 데이터를 계속 보내면 런타임 패닉이 발생합니다. nil 채널에 데이터를 보내면 무한정 블록됩니다.
receive 연산자
<-ch는 채널 ch에서 데이터를 수신하는 데 사용됩니다. 이 표현식은 수신할 데이터가 있을 때까지 블록됩니다. nil 채널에서 데이터를 수신하면 무한정 블록됩니다. 닫힌 채널에서 데이터를 수신하면 블록되지 않지만 즉시 반환됩니다. 전송된 데이터를 수신한 후에는 요소 타입의 제로 값이 반환됩니다. 앞서 언급했듯이 추가 반환 매개변수를 사용하여 채널이 닫혔는지 여부를 확인할 수 있습니다.
x, ok := <-ch x, ok = <-ch var x, ok = <-ch
OK가 false이면 수신된 x가 생성된 제로 값이고 채널이 닫히거나 비어 있음을 나타냅니다.
블로킹
기본적으로 송신 및 수신은 상대방이 준비될 때까지 블록됩니다. 이 방법은 명시적 잠금 또는 조건 변수를 사용하지 않고도 고루틴에서 동기화하는 데 사용할 수 있습니다. 예를 들어, 공식 예제:
import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to 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) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
위 코드에서 x, y := <-c, <-c 문은 계산 결과가 채널로 전송될 때까지 계속 대기합니다.
버퍼링된 채널
make의 두 번째 매개변수는 버퍼의 크기를 지정합니다.
ch := make(chan int, 100)
버퍼를 사용하면 블로킹을 최대한 피하고 애플리케이션 성능을 향상시킬 수 있습니다.
Range
for …… range 문은 채널을 처리할 수 있습니다.
func main() { go func() { time.Sleep(1 * time.Hour) }() c := make(chan int) go func() { for i := 0; i < 10; i = i + 1 { c <- i } close(c) }() for i := range c { fmt.Println(i) } fmt.Println("Finished") }
range c에 의해 생성된 반복 값은 Channel에서 전송된 값입니다. 채널이 닫힐 때까지 계속 반복됩니다. 위 예제에서 close(c)가 주석 처리되면 프로그램은 for …… range 줄에서 블록됩니다.
select
select 문은 가능한 송수신 작업 집합을 선택하고 처리하는 데 사용됩니다. 이는 switch와 유사하지만 통신 작업만 처리하는 데 사용됩니다. 이의 case는 send 문, receive 문 또는 default일 수 있습니다. receive 문은 값을 하나 또는 두 개의 변수에 할당할 수 있으며 receive 작업이어야 합니다. 최대 하나의 default 케이스가 허용되며, 일반적으로 케이스 목록의 끝에 배치됩니다. 예를 들어:
import "fmt" func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
여러 case를 동시에 처리할 수 있는 경우(예: 여러 채널이 동시에 데이터를 수신할 수 있는 경우) Go는 의사 - 임의로 처리할 case를 선택합니다(의사 - 임의). 처리해야 할 case가 없으면 default가 선택되어 처리됩니다( default case가 있는 경우). default case가 없으면 select 문은 처리해야 할 case가 있을 때까지 블록됩니다. nil 채널에 대한 작업은 무한정 블록됩니다. default case가 없으면 nil 채널만 있는 select는 무한정 블록됩니다. select 문은 switch 문과 마찬가지로 루프가 아니며 하나의 case만 선택하여 처리합니다. 채널을 계속 처리하려면 외부에 무한 for 루프를 추가할 수 있습니다.
for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } }
타임아웃
select의 중요한 응용 프로그램 중 하나는 타임아웃 처리입니다. select 문은 처리해야 할 case가 없으면 블록되므로 이 때 타임아웃 작업이 필요할 수 있습니다. 예를 들어:
import "time" import "fmt" func main() { c1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second * 1): fmt.Println("timeout 1") } }
위 예제에서는 2초 후에 채널 c1로 데이터가 전송되지만 select는 1초 후에 타임아웃되도록 설정됩니다. 따라서 result 1 대신 timeout 1이 인쇄됩니다. 이는 time.After 메서드를 사용하며, 이 메서드는 <-chan Time 타입의 단방향 채널을 반환합니다. 지정된 시간에 현재 시간이 반환된 채널로 전송됩니다.
타이머 및 티커
- 타이머: 이는 단일 미래 이벤트를 나타내는 타이머입니다. 대기 시간을 지정할 수 있으며 채널을 제공합니다. 지정된 시간에 채널은 시간 값을 제공합니다. 예를 들어:
timer1 := time.NewTimer(time.Second * 2) <-timer1.C fmt.Println("Timer 1 expired")
위의 두 번째 줄은 시간이 될 때까지 약 2초 동안 블록된 다음 계속 실행됩니다. 물론 간단히 기다리고 싶다면 time.Sleep을 사용하여 달성할 수 있습니다. timer.Stop을 사용하여 타이머를 중지할 수도 있습니다.
timer2 := time.NewTimer(time.Second) go func() { <-timer2.C fmt.Println("Timer 2 expired") }() stop2 := timer2.Stop() if stop2 { fmt.Println("Timer 2 stopped") }
- 티커: 이는 정기적으로 트리거되는 타이머입니다. 간격(interval)로 채널에 이벤트(현재 시간)를 보냅니다. 채널의 수신자는 고정된 시간 간격으로 채널에서 이벤트를 읽을 수 있습니다. 예를 들어:
ticker := time.NewTicker(time.Millisecond * 500) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } }()
timer와 유사하게 ticker도 Stop 메서드를 통해 중지할 수 있습니다. 중지되면 수신자는 더 이상 채널에서 데이터를 수신하지 않습니다.
close
내장된 close 메서드를 사용하여 채널을 닫을 수 있습니다. 채널이 닫힌 후 송신자와 수신자의 작업을 요약합니다.
- 채널
c가 닫히면 계속해서 데이터를 보내면 패닉이 발생합니다:send on closed channel. 예를 들어:
import "time" func main() { go func() { time.Sleep(time.Hour) }() c := make(chan int, 10) c <- 1 c <- 2 close(c) c <- 3 }
- 닫힌 채널에서 보낸 데이터를 읽을 수 있을 뿐만 아니라 제로 값을 계속 읽을 수도 있습니다.
c := make(chan int, 10) c <- 1 c <- 2 close(c) fmt.Println(<-c) //1 fmt.Println(<-c) //2 fmt.Println(<-c) //0 fmt.Println(<-c) //0
range를 통해 읽으면 채널이 닫힌 후for루프가 점프합니다.
c := make(chan int, 10) c <- 1 c <- 2 close(c) for i := range c { fmt.Println(i) }
i, ok := <-c를 통해 채널의 상태를 확인하고 값이 제로 값인지 또는 정상적으로 읽은 값인지 확인할 수 있습니다.
c := make(chan int, 10) close(c) i, ok := <-c fmt.Printf("%d, %t", i, ok) //0, false
동기화
채널은 고루틴 간의 동기화에 사용할 수 있습니다. 예를 들어, 다음 예제에서 메인 고루틴은 done 채널을 통해 작업자가 작업을 완료할 때까지 기다립니다. 작업자는 작업을 완료한 후 채널로 데이터를 보내 메인 고루틴에 작업이 완료되었음을 알릴 수 있습니다.
import ( "fmt" "time" ) func worker(done chan bool) { time.Sleep(time.Second) // Notify that the task is completed done <- true } func main() { done := make(chan bool, 1) go worker(done) // Wait for the task to complete <-done }
Leapcell: Golang 호스팅에 가장 적합한 서버리스 플랫폼
마지막으로, Golang 배포에 가장 적합한 플랫폼인 Leapcell을 추천합니다.

1. 다중 - 언어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 따라서만 지불하십시오 — 요청 없음, 요금 없음.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 개의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 쉬운 동시성 처리를 위한 자동 - 확장.
- 제로 운영 오버헤드 — 구축에만 집중하십시오.

Leapcell Twitter: https://x.com/LeapcellHQ

