고루틴 <-> 채널 <->고루틴
채널에 값을 보내고 꺼냄
채널은 다음과 같은 형식으로 사용
make(chan 자료형)
고루틴과 채널을 사용하여 두 정수 값을 더하기
package main
import "fmt"
func sum(a int, b int, c chan int) {
c <- a + b // 채널에 a와 b의 합을 보냄
}
func main() {
c := make(chan int) // int형 채널 생성
go sum(1, 2, c) // sum을 고루틴으로 실행한 뒤 채널을 매개변수로 넘겨줌
n := <-c // 채널에서 값을 꺼낸 뒤 n에 대입
fmt.Println(n) // 3
}
채널을 사용하기 전에는 반드시 make 함수로 공간을 할당해야 한다. 그리고 이렇게 생성하면 동기 채널(synchronous channel)이 생성된다.
다음과 같이 :=를 사용하지 않고, var 키워드로 채널을 선언하고 할당할 수도 있다
var c chan int // chan int형 변수 선언
c = make(chan int)
sum 함수를 고루틴으로 실행하면서 더할 값과 채널 변수를 넣는다. 채널을 매개변수로 받는 함수는 반드시 go 키워드를 사용하여 고루틴으로 실행해야 한다
함수에서 채널을 매개변수로 받을 때는 다음과 같은 형식으로 사용한다.
변수명 chan 자료형
// ↓ int형 채널을 매개변수로 받음
func sum(a int, b int, c chan int) {
c <- a + b
// ↑ 채널에 값을 보냄
}
채널에 값을 보낼 때는 다음과 같은 형식으로 사용한다.
채널 <- 값
채널 변수에는 =로 값을 대입하지 않고 <- 연산자를 사용한다. 여기서는 sum 함수 안에서 a와 b를 더한 값을 채널 c로 보낸다
이제 main 함수에서는 채널에서 값을 가져온다
<- 채널
n := <-c
이번에도 <- 연산자를 사용하지만 순서가 반대로 되어있다. 즉 채널 c에서 값을 가져온 뒤 변수 n을 생성하여 대입한다(fmt.Println(<-c)처럼 변수에 대입하지 않고 바로 사용할 수도 있다).
<-c는 채널에서 값이 들어올 때까지 대기한다. 그리고 채널에 값이 들어오면 대기를 끝내고 다음 코드를 실행한다. 따라서 채널은 값을 주고 받는 동시에 동기화 역할까지 수행한다.
요약하자면 다음과 같다.
채널 <-: 채널에 값을 보냅니다.
<- 채널: 채널에 값이 들어올 때까지 기다린 뒤 값이 들어오면 값을 가져옵니다.
가져온 값은 :=, =를 사용하여 변수에 대입할 수 있다.
값을 가져오면 다음 코드를 실행한다.
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan bool) // 동기 채널 생성
count := 3 // 반복할 횟수
go func() {
for i := 0; i < count; i++ {
done <- true // 고루틴에 true를 보냄, 값을 꺼낼 때까지 대기
fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
time.Sleep(1 * time.Second) // 1초 대기
}
}()
for i := 0; i < count; i++ {
<-done // 채널에 값이 들어올 때까지 대기, 값을 꺼냄
fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
}
}
make(chan bool)처럼 채널을 생성합니다. 여기서는 채널로 값을 주고 받아도 실제로 사용하지 않으므로 자료형은 큰 의미가 없다. make 함수에 매개 변수를 하나만 지정했으므로 동기 채널을 생성한다.
먼저 고루틴을 생성하고, 반복문을 실행할 때마다 채널 done에 true 값을 보낸 뒤 1초를 기다린다
go func() {
for i := 0; i < count; i++ {
done <- true // 고루틴에 true를 보냄, 값을 꺼낼 때까지 대기
fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
time.Sleep(1 * time.Second) // 1초 대기
}
}()
동기 채널이므로 done에 값을 보내면 다른 쪽에서 값을 꺼낼 때까지 대기한다. 따라서 반복문도 실행되지 않으므로 “고루틴 : 숫자”가 계속 출력되지 않는다.
이제 메인 함수에서는 반복문을 실행할 때마다 채널 done에서 값을 꺼낸다
for i := 0; i < count; i++ {
<-done // 채널에 값이 들어올 때까지 대기, 값을 꺼냄
fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
}
고루틴 : 0
메인 함수 : 0
고루틴 : 1
메인 함수 : 1
고루틴 : 2
메인 함수 : 2
다음은 채널의 버퍼가 가득차면 값을 꺼내서 출력한다
make(chan 자료형, 버퍼_개수)
package main
import (
"fmt"
"runtime"
)
func main() {
runtime.GOMAXPROCS(1)
done := make(chan bool, 2) // 버퍼가 2개인 비동기 채널 생성
count := 4 // 반복할 횟수
go func() {
for i := 0; i < count; i++ {
done <- true // 채널에 true를 보냄, 버퍼가 가득차면 대기
fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
}
}()
for i := 0; i < count; i++ {
<-done // 버퍼에 값이 없으면 대기, 값을 꺼냄
fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
}
}
채널에 버퍼를 1개 이상 설정하면 비동기 채널(asynchronous channel)이 생성된다. 비동기 채널은 보내는 쪽에서 버퍼가 가득 차면 실행을 멈추고 대기하며 받는 쪽에서는 버퍼에 값이 없으면 대기한다.
고루틴을 생성하고, 반복문을 실행할 때마다 채널 done에 true 값을 보낸다.
go func() {
for i := 0; i < count; i++ {
done <- true // 채널에 true를 보냄, 버퍼가 가득차면 대기
fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
}
}()
비동기 채널이므로 버퍼가 가득 찰때까지 값을 계속 보낸다. 여기서는 채널의 버퍼를 2개로 설정했으므로 done에 true를 2번 보낸 뒤 그 다음 루프에서 대기한다(버퍼가 가득 차면 대기하므로 i가 0, 1일 때 채널에 값을 보낸 뒤 i가 2일때 done <- true에서 대기한다).
이제 메인 함수에서는 반복문을 실행할 때마다 채널 done에서 값을 꺼낸다.
for i := 0; i < count; i++ {
<-done // 버퍼에 값이 없으면 대기, 값을 꺼냄
fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
}
고루틴 : 0
고루틴 : 1
메인 함수 : 0
메인 함수 : 1
고루틴 : 2
고루틴 : 3
메인 함수 : 2
메인 함수 : 3
package main
import "fmt"
func main() {
c := make(chan int) // int형 채널을 생성
go func() {
for i := 0; i < 5; i++ {
c <- i // 채널에 값을 보냄
}
close(c) // 채널을 닫음
}()
for i := range c { // range를 사용하여 채널에서 값을 꺼냄
fmt.Println(i) // 꺼낸 값을 출력
}
}
0
1
2
3
4
for 반복문 안에서 range 키워드를 사용하면 채널이 닫힐 때까지 반복하면서 값을 꺼낸다. 여기서는 동시에 고루틴 안에서 채널 c에 0부터 4까지 값을 보낸 뒤 close를 사용하여 채널을 닫는다. 이렇게 하면 range로 0부터 4까지 꺼내고, 값을 출력한 뒤 반복문이 종료된다.
range와 close 함수의 특징
이미 닫힌 채널에 값을 보내면 패닉이 발생한다
채널을 닫으면 range 루프가 종료된다.
채널이 열려있고, 값이 들어오지 않는다면 range는 실행되지 않고 계속 대기한다 만약 다른 곳에서 채널에 값을 보냈다면(채널에 값이 들어오면) 그때부터 range가 계속 반복된다
정리하자면 range는 채널에 값이 몇 개나 들어올지 모르기 때문에 값이 들어올 때마다 계속 꺼내기 위해 사용한다
채널을 가져온 뒤 두 번째 리턴값으로 채널이 닫혔는지 확인할 수 있
c := make(chan int, 1)
go func() {
c <- 1
}()
a, ok := <-c // 채널이 닫혔는지 확인
fmt.Println(a, ok) // 1 true: 채널은 열려 있고 1을 꺼냄
close(c) // 채널을 닫음
a, ok = <-c // 채널이 닫혔는지 확인
fmt.Println(a, ok) // 0 false: 채널이 닫혔음
보내기 전용 채널과 받기 전용 채널은 값의 흐름이 한 방향으로 고정된 채널이다
다음은 0부터 4까지 채널에 값을 보내고, 다시 채널에서 값을 꺼내서 출력한다. 그리고 반복문이 끝난 뒤 채널에 100을 보낸 뒤 다시 꺼내서 출력한다
package main
import "fmt"
func producer(c chan<- int) { // 보내기 전용 채널
for i := 0; i < 5; i++ {
c <- i
}
c <- 100 // 채널에 값을 보냄
//fmt.Println(<-c) // 채널에서 값을 꺼내면 컴파일 에러
}
func consumer(c <-chan int) { // 받기 전용 채널
for i := range c {
fmt.Println(i)
}
fmt.Println(<-c) // 채널에 값을 꺼냄
// c <- 1 // 채널에 값을 보내면 컴파일 에러
}
func main() {
c := make(chan int)
go producer(c)
go consumer(c)
fmt.Scanln()
}
0
1
2
3
4
100
보내기 전용 및 받기 전용 채널은 채널 앞 뒤로 <- 연산자를 붙여서 만듭니다. 보통 함수의 매개변수로 사용하거나, 구조체의 필드로 사용한다.
보내기 전용(send-only): chan<- 자료형 형식이다. c chan<- int는 int형 보내기 전용 채널 c를 뜻한다. 보내기 전용 채널은 값을 보낼 수만 있으며 값을 가져오려고 하면 컴파일 에러가 발생한다.
받기 전용(receive-only): <-chan 자료형 형식이다. c <-chan int는 int형 받기 전용 채널 c를 뜻한다. 받기 전용 채널은 range 키워드 또는 <- 채널 형식으로 값을 꺼낼 수만 있으며 값을 보내려고 하면 컴파일 에러가 발생한다.
chan 키워드를 기준으로 <- (화살표)가 붙은 방향을 보면 보내기 전용인지 받기 전용인지 알 수 있다. 즉 chan<-은 chan 키워드로 <-가 들어가므로 보내기 전용, <-chan은 chan 키워드에서 <-가 나오고 있으므로 받기 전용 채널이다.
여기서는 producer 함수는 매개변수로 보내기 전용 채널을 사용하고, consumer 함수는 매개변수로 받기 전용 채널을 사용한다. 따라서 producer 함수는 값을 보내기만 하고, consumer 함수는 값을 꺼내기만 한다.
이번에는 채널을 리턴값으로 사용하여, 다음은 두 수를 더한 뒤 채널로 리턴한다.
package main
import "fmt"
// ↓ 함수의 리턴 값은 int 형 받기 전용 채널
func sum(a, b int) <-chan int {
out := make(chan int) // 채널 생성
go func() {
out <- a + b // 채널에 a와 b의 합을 보냄
}()
return out // 채널 변수 자체를 리턴
}
func main() {
c := sum(1, 2) // 채널을 리턴값으로 받아서 c에 대입
fmt.Println(<-c) // 3: 채널에서 값을 꺼냄
}
sum 함수는 받기 전용 채널을 리턴하도록 만들었고. 채널을 리턴하려면 먼저 make 함수로 채널을 생성한다 그리고 고루틴 안에서 채널에 값을 보낸 뒤 고루틴 바깥에서 채널을 리턴한다
sum 함수를 사용하여 채널을 리턴값으로 받았으면 <-c처럼 값을 꺼내면 된다.
이번에는 채널만 사용하여 값을 더해보겠다.
package main
import "fmt"
// ↓ 함수의 리턴 값은 int 형 받기 전용 채널
func num(a, b int) <-chan int {
out := make(chan int) // int형 채널 생성
go func() {
out <- a // 채널에 a의 값을 보냄
out <- b // 채널에 b의 값을 보냄
close(out) // 채널을 닫음
}()
return out // 채널 변수 자체를 리턴
}
// ↓ 함수의 매개변수는 int형 받기 전용 채널
func sum(c <-chan int) <-chan int {
// ↑ 함수의 리턴 값은 int형 받기 전용 채널
out := make(chan int) // int형 채널 생성
go func() {
r := 0
for i := range c { // range를 사용하여 채널이 닫힐 때까지 값을 꺼냄
r = r + i // 꺼낸 값을 모두 더함
}
out <- r // 더한 결과를 채널에 보냄
}()
return out // 채널 변수 자체를 리턴
}
func main() {
c := num(1, 2) // 1과 2가 들어있는 채널이 리턴됨
out := sum(c) // 채널 c를 매개변수에 넘겨서 모두 더함, 더한 값이 들어있는 out 채널을 리턴
fmt.Println(<-out) // 3: out 채널에서 값을 꺼냄
}
func num(a, b int) <-chan int {
out := make(chan int) // int형 채널 생성
go func() {
out <- a // 채널에 a의 값을 보냄
out <- b // 채널에 b의 값을 보냄
close(out) // 채널을 닫음
}()
return out // 채널 변수 자체를 리턴
}
func sum(c <-chan int) <-chan int {
out := make(chan int) // int형 채널 생성
go func() {
r := 0
for i := range c { // range를 사용하여 채널이 닫힐 때까지 값을 꺼냄
r = r + i // 꺼낸 값을 모두 더함
}
out <- r // 더한 결과를 채널에 보냄
}()
return out // 채널 변수 자체를 리턴
}
c := num(1, 2) // 1과 2가 들어있는 채널이 리턴됨
out := sum(c) // 채널 c를 매개변수에 넘겨서 모두 더함, 더한 값이 들어있는 out 채널을 리턴
fmt.Println(<-out) // 3: out 채널에서 값을 꺼냄
select {
case <-채널1:
// 채널1에 값이 들어왔을 때 실행할 코드를 작성합니다.
case <-채널2:
// 채널2에 값이 들어왔을 때 실행할 코드를 작성합니다.
default:
// 모든 case의 채널에 값이 들어오지 않았을 때 실행할 코드를 작성합니다.
}
select 분기문은 switch 분기문과 비슷하지만 select 키워드 뒤에 검사할 변수를 따로 지정하지 않으며 각 채널에 값이 들어오면 해당 case가 실행된다.(close 함수로 채널을 닫았을 때도 case가 실행된다.). 그리고 보통 select를 계속 처리할 수 있도록 for로 반복해준다(반복하지 않으면 한 번만 실행하고 끝낸다).
switch 분기문과 마찬가지로 select 분기문도 default 케이스를 지정할 수 있으며 case에 지정된 채널에 값이 들어오지 않았을 때 즉시 실행된다. 단, default에 적절한 처리를 하지 않으면 CPU 코어를 모두 점유하므로 주의한다
다음은 채널 2개를 생성하고 100밀리초, 500밀리초 간격으로 숫자와 문자열을 보낸 뒤 꺼내서 출력한다
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan int) // int형 채널 생성
c2 := make(chan string) // string 채널 생성
go func() {
for {
c1 <- 10 // 채널 c1에 10을 보낸 뒤
time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
}
}()
go func() {
for {
c2 <- "Hello, world!" // 채널 c2에 Hello, world!를 보낸 뒤
time.Sleep(500 * time.Millisecond) // 500 밀리초 대기
}
}()
go func() {
for {
select {
case i := <-c1: // 채널 c1에 값이 들어왔다면 값을 꺼내서 i에 대입
fmt.Println("c1 :", i) // i 값을 출력
case s := <-c2: // 채널 c2에 값이 들어왔다면 값을 꺼내서 s에 대입
fmt.Println("c2 :", s) // s 값을 출력
}
}
}()
time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
}
c2 : Hello, world!
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c2 : Hello, world!
c1 : 10
... (생략)
select {
case i := <-c1: // 채널 c1에 값이 들어왔다면 값을 꺼내서 i에 대입
fmt.Println("c1 :", i) // i 값을 출력
case s := <-c2: // 채널 c2에 값이 들어왔다면 값을 꺼내서 s에 대입
fmt.Println("c2 :", s) // s 값을 출력
}
select {
case i := <-c1:
fmt.Println("c1 : ", i)
case s := <-c2:
fmt.Println("c2 : ", s)
case <-time.After(50 * time.Millisecond): // 50 밀리초 후 현재 시간이 담긴 채널이 리턴됨
fmt.Println("timeout")
}
이처럼 case에서는 time.After와 같이 받기 전용 채널을 리턴하는 함수를 사용할 수 있다.
select 분기문은 채널에 값을 보낼 수도 있다.
case 채널 <- 값: 코드
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan int) // int형 채널 생성
c2 := make(chan string) // string 채널 생성
go func() {
for {
i := <-c1 // 채널 c1에서 값을 꺼낸 뒤 i에 대입
fmt.Println("c1 :", i) // i 값을 출력
time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
}
}()
go func() {
for {
c2 <- "Hello, world!" // 채널 c2에 Hello, world!를 보냄
time.Sleep(500 * time.Millisecond) // 500 밀리초 대기
}
}()
go func() {
for { // 무한 루프
select {
case c1 <- 10: // 매번 채널 c1에 10을 보냄
case s := <-c2: // c2에 값이 들어왔을 때는 값을 꺼낸 뒤 s에 대입
fmt.Println("c2 :", s) // s 값을 출력
}
}
}()
time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
}
c2 : Hello, world!
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c2 : Hello, world!
c1 : 10
... (생략)
for { // 무한 루프
select {
case c1 <- 10: // 매번 채널 c1에 10을 보냄
case s := <-c2: // c2에 값이 들어왔을 때는 값을 꺼낸 뒤 s에 대입
fmt.Println("c2 :", s) // s 값을 출력
}
}
있습니다.
c1 := make(chan int) // int형 채널 생성
go func() {
for {
i := <-c1 // 채널 c1에서 값을 꺼낸 뒤 i에 대입
fmt.Println("c1 :", i) // i 값을 출력
time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
}
}()
go func() {
for {
c1 <- 20 // 채널 c1에 20을 보냄
time.Sleep(500 * time.Millisecond) // 100 밀리초 대기
}
}()
go func() {
for { // 무한 루프
select { // 채널 c1 한 개로 값을 보내거나 받음
case c1 <- 10: // 매번 채널 c1에 10을 보냄
case i := <-c1: // c1에 값이 들어왔을 때는 값을 꺼낸 뒤 i에 대입
fmt.Println("c1 :", i) // i 값을 출력
}
}
}()
time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
c1 : 20
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c1 : 20
c1 : 10
... (생략)