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