본문 바로가기
Study/Go

[Go] 고루틴(GoRoutine) 심화(4) - channel

by _royJang 2022. 5. 15.

Channel

Channel 이란?

고루틴을 사용하는 데 있어서 데이터를 전달하는 가장 효과적인 방법이다. 데이터를 전달하는데 통로 역할을 한다. 채널을 사용하는 데 있어 서로 간에 많은 정보를 알 필요는 없으며 채널에 대한 주소 값만 알면 채널을 통해 데이터를 송신하고 수신할 수 있다.

Channel의 종류

  1. 양방향 채널 (chan interface{})
    데이터를 송신하고 수신하는 역할 모두 가능하다.
  2. 송신용 채널 (chan <- interface{})
    데이터를 송신하는 역할만이 가능하다.
  3. 수신용 채널 (<- chan interface{})
    데이터를 수신하는 역할만이 가능하다.
  4. nil
    인스턴스화가 진행되지 않고 선언만이 진행된 상태이다.

한 가지 역할만을 하는 단방향 채널은 화살표의 방향이 채널을 가리키면 채널에게 값을 전달하는 송신용, 채널에서 화살표가 나온다면 채널을 통해 값을 받는 수신용으로 생각하면 좋을 듯하다.

Channel의 생성

var testChan chan interface{}
testChan = make(chan interface{})

다른 값들과 마찬가지로 := 를 통해 선언과 초기화를 동시에 진행하는 것이 가능하며 예제와 같이 분리하여 나누는 것도 가능하다.

또한 양방향으로 채널을 선언하였지만 단방향 채널로 인스턴스화 하는 것도 가능하다.

var testChan chan interface{}
testChan = make(chan<- interface{})

이런 식으로 말이다.

Channel의 사용 예시

예시 코드

package main

import "fmt"

func main() {
    stringChan := make(chan string)
    go func() {
        stringChan <- "Hello World!"
    }()
    fmt.Println(<-stringChan)
}

실행 결과

Hello World!

어쩌면 의아함을 가질 수 있는 결과이다. 분명 고루틴을 위와 같은 방법을 통해 실행시켰을 때 메인 고루틴이 먼저 종료가 되어 익명 고루틴이 실행되지 못했던 경험을 했었을 수 있으니. 그러나 채널은 블락 형태로 진행되기에 이 부분에서 확실히 실행을 기대할 수 있다. 이 부분은 나의 이전 포스팅을 살펴보면 도움이 될 것이다.

Channel의 close

예시 코드

package main

import "fmt"

func main() {
    stringChan := make(chan string)
    go func() {
        stringChan <- "Hello World!"
    }()
    val, ok := <-stringChan
    fmt.Printf("%v : %v\n", val, ok)
    close(stringChan)
    val, ok = <-stringChan
    fmt.Printf("%v : %v", val, ok)
}

실행 결과

Hello World! : true
 : false

채널 값을 수신할 때에는 두 개의 값을 return 하는데 채널을 통해 수신한 값과 현재 채널의 상태가 그것이다. 예제에서 볼 수 있듯 채널이 닫힌 상태에서도 값을 읽을 수 있으며 이때에 수신받는 값은 타입의 기본값이다(여기선 "" - 빈 스트링).
채널의 close를 통해 다양한 패턴을 사용할 수 있다.

for문을 통해 chan을 읽는 것이 한 예이다.

for문을 사용하는 예시 코드

package main

import "fmt"

func main() {
    intChan := make(chan int)
    go func() {
        defer close(intChan)
        for i := 0; i < 5; i++ {
            intChan <- i
        }
    }()
    for i := range intChan {
        fmt.Printf("%d ", i)
    }
}

실행 결과

0 1 2 3 4 

정상적으로 실행되고 원하는 결괏값을 얻는 것을 볼 수 있다.
만일 채널을 close 하지 않는다면

fatal error: all goroutines are asleep - deadlock!

데드락에 걸리는 모습을 볼 수 있을 것이다.

또한 대기 중인 채널 모두에게 대기를 해제하게 할 수도 있다.

대기 해제 예시 코드

package main

import (
    "fmt"
    "sync"
)

func main() {
    begin := make(chan interface{})
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            <-begin
            fmt.Printf("%v has begun\n", i)
        }(i)
    }
    fmt.Println("Unblocking goroutines....")
    close(begin)
    wg.Wait()
}

실행 결과

Unblocking goroutines....
0 has begun
2 has begun
3 has begun
4 has begun
1 has begun

고루틴은 <-begin 부분에서 대기를 진행하고 있다. 그렇기에 고루틴이 닫히기 전엔 어떠한 동작도 하지 않는다. 그리고 채널이 닫히면 값을 출력하는 것을 볼 수 있다.

마무리

채널..

고루틴을 어떻게 활용하는가에 가장 큰 역할을 하는 친구 같다.

이 친구를 정복해야 Go를 잘 사용한다고 말할 수 있지 않을까 싶다.