Day 8 with Go-Lang

3 minute read

Go 채널

Go 채널은 데이터를 주고받는 통로라 볼 수 있는데, 채널은 make() 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 를 통해 데이터를 보내고 받는다. 채널은 흔히 goroutine 사이의 데이터를 주고받는데 사용되는데, 상대방이 준비될 때까지 채널에서 대기함으로써 별도 Lock 을 걸지 않고 데이터를 동기화하는 데 사용된다.

아래 예제는 정수형 채널을 생성하고, 한 goroutine 에서 그 채널에 123 이란 정수 데이터를 보낸 후, 이를 다시 메인 routine 에서 데이터를 받는 코드이다. 채널을 생성할 때는 make() 함수에 어떤 타입의 데이터를 주고 받을 지 미리 정해야 한다. 채널로 데이터를 보낼 때는 채널명 <- 데이터 와 같이 사용하고, 채널로부터 데이터를 받을 경우는 <- 채널명 과 같이 사용한다.

package main

func main() {
	ch := make(chan int)

	go func() {
		ch <- 123
	}()

	i := <- ch
	println(i)
}

Go 채널은 수신자와 송신자가 서로를 기다리는 속성 때문에, 이를 이용해 Go 루틴이 끝날 때까지 기다리는 기능을 구현할 수 있다. 즉, 익명함수를 사용한 한 Go 루틴에서 어떤 작업이 실행되고 있을 때, 메인 루틴은 <- done 에서 계속 수신하며 대기하게 된다. 익명함수 루틴에서 작업이 끝난 후, done 채널에 true 를 보내면, 수신자 메인루틴은 이를 받고 프로그램을 종료하게 된다.

package main

func main() {
	done := make(chan bool)
	go func() {
		for i := 0; i<10; i++ {
			println(i)
		}
		done <- true
	}()

	// 위의 Go 루틴이 끝날 때까지 대기
	<-done
}
  • Go 채널 버퍼링

Go 채널은 2가지가 있는데, Unbuffered Channel 과 Buffered Channel 이 있다. 위의 예제에서 Go 채널은 Unbuffered Channel 로서 이 채널에서는 하나의 수신자가 데이터를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여있게 된다. 하지만, Buffered Channel 을 사용하면 비록 수신자가 받을 준비가 되어 있지 않을 지라도 지정된 버퍼만큼 데이터를 보내고 계쏙 다른 일을 수행할 수 있다. 버퍼 채널은 make(chan type, N) 함수를 통해 생성되는데, 두 번째 Parameter 에 사용할 버퍼 갯수를 넣는다.

버퍼 채널을 이용하지 않는 경우, 아래의 코드는 에러 (fatal error: all goroutines are asleep - deadlock!) 를 발생시킨다. 왜냐하면 메인 루틴에서 채널에 1을 보내며 수신자를 기다리고 있는데, 이 채널을 받는 수신자 루틴이 없기 때문이다.

package main

func main() {
	c := make(chan int)
	c <- 1 // 수신 루틴이 없으므로 데드락
	println(<-c) // 코멘트 해도 데드락 (별도의 Go 루틴이 없기 때문)
}

하지만 아래와 같이 버퍼채널을 사용하면, 수신자가 당장 없더라도 에러가 발생하지 않는다.

package main

func main() {
	ch := make(chan int, 1)

	ch <- 101

	println(<-ch)
}
  • 채널 파라미터

채널을 함수의 파라미터로 전달할 때, 일반적으로 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 것인지 혹은 수신만 할 것인지를 지정할 수 있다. 송신 파라미터는 (p chan<- int) 와 같이 chan<- 을 사용하고, 수신 파라미터는 (p <-chan int) 와 같이 <-chan 을 사용한다. 만약 송신 채널 파라미터에서 수신을 하거나, 수신 채널에 송신을 하게 되면 에러가 발생한다.

package main

func main() {
	ch := make(chan string, 1)

	sendChan(ch)
	receiveChan(ch)
}

func sendChan(ch chan<- string) {
	ch <- "Data"
	// x := <-ch // 에러발생
}

func receiveChan(ch <-chan string) {
	data := <-ch
	println(data)
}
  • 채널 닫기

채널을 오픈한 후, close() 함수를 사용해 채널을 닫을 수 있다. 채널을 닫게 되면, 해당 채널로 더이상 송신은 불가하지만 수신은 가능하다. 채널 수신에 사용되는 <-ch 은 두개의 리턴값을 갖는데, 첫 번째는 채널 메시지이고, 두 번째는 수신이 제대로 되었는지를 나타낸다. 채널의 데이터가 더 이상 없다면, 두 번째 리턴값은 false 를 리턴한다.

package main

func main() {
	ch := make(chan int, 2)

	// 채널에 송신
	ch <- 1
	ch <- 2

	// 채널을 닫는다
	close(ch)

	println(<-ch)
	println(<-ch)

	if _, success := <-ch; !success {
		println("더 이상 데이터 없음")
	}
}
  • 채널 Range 문

채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있다. 그리고 수신자는 임의의 갯수의 데이터를 채널이 닫힐 때까지 계속 수신할 수 있다. 아래 예제는 이러한 송수신 방법을 표현한 것으로, 수신자는 채널이 닫힌 것을 체크하면서 계속 루프를 돌게 된다. 방법1은 무한 for loop 안에서 if 문으로 수신 채널의 두 번째 파라미터를 체크하는 방식이고, 방법2는 방법1과 동일한 표현이지만, for range 문으로 보다 간결하게 표현한 것이다. 채널 range 문은 range 키워드 다음의 채널로부터 계속 수신하다 채널이 닫힌 것을 감지하면 for 루프를 종료한다.

package main

func main() {
	ch := make(chan int, 2)

	ch <- 1
	ch <- 2

	close(ch)

	// 방법 1
	// 채널이 닫힌 것을 감지할 때까지 계속 수신
	//for {
	//	if i, success := <-ch; success {
	//		println(i)
	//	} else {
	//		break
	//	}
	//}

	// 방법 2
	// 위 표현과 동일한 채널 range 문
	for i := range ch {
		println(i)
	}
}
  • 채널 select 문

Go 의 select 문은 복수 채널을 기다리며 준비된 채널을 실행하는 기능을 제공한다. 즉, select 문은 여러 개의 case 문에서 각각 다른 채널을 기다리다가 준비가 된 채널 case 를 실행한다. select 문은 case 채널들이 준비되지 않으면 계속 대기하게 되고, 가장 먼저 도착한 채널의 case 를 실행한다. 만약 복수 채널에 신호가 오면, Go 런타임이 랜덤하게 그 중 하나를 선택한다. 하지만, select 문에 default 문이 있으면, case 문 채널이 준비되지 않더라도 계속 대기하지 않고 바로 default 문을 실행한다.

아래 예제는 for 루프 안에 select 문을 쓰면서 두 개의 goroutine 이 모두 실행되기를 기다린다. run1() 이 1초간 실행되고, done1 채널로부터 수신해 해당 case 를 실행하고 다시 for Loop 를 돈다. for Loop 를 다시 돌며 select 문이 실행되는데, 다음 run2() 가 2초 후에 실행되고 done2 채널로부터 수신해 해당 case 를 실행하게 된다. done2 채널 case 문에 break EXIT 이 있는데, 이 문장으로 인해 for Loop 를 빠져나와 EXIT 레이블로 이동하게 된다.

package main

import "time"

func main() {
	done1 := make(chan bool)
	done2 := make(chan bool)

	go run1(done1)
	go run2(done2)

EXIT:
	for {
		select {
		case <-done1:
			println("Run1 완료")
		case <-done2:
			println("Run2 완료")
			break EXIT
		default:
			println(".")
			time.Sleep(time.Millisecond * 100)
		}
	}
}

func run1(done chan<- bool) {
	time.Sleep(1 * time.Second)
	done <- true
}

func run2(done chan<- bool) {
	time.Sleep(2 * time.Second)
	done <- true
}

예제로 배우는 Go 프로그래밍 의 Go 기초 과정 완료!

Reference : 예제로 배우는 Go 프로그래밍