본문으로 바로가기

 이전에 작성했던 글의 마지막 부분인 RoutinesChannels에 대해 추가로 정리

[IT/Go] - `쉽고 빠른 Go 시작하기` 노마드코더 강의 정리 (Golang)

 

`쉽고 빠른 Go 시작하기` 노마드코더 강의 정리 (Golang)

오랜만에 nomad coder 강의를 들으며 Go 언어를 새로 공부해 보았다. 지금부터 기재하는 내용은 모두 nomadcoders 쉽고 빠른 Go 시작하기에서 강의를 무려 무료로 !! 들을 수 있으니 모두 시간이 된다면

jeffrey-oh.tistory.com

Go Routines

일반적으로 프로그램은 동시에 진행되지 않는다. 위에서 아래로 흐를 뿐

예를들면,

func main() {
  checkCount("jeffrey")
  checkCount("oh")
}

func checkCount(word string) {
  for i:=0; i<10; i++ {
    fmt.Println("Welcome ! ", word)
    time.Sleep(time.Second) // time 함수를 이용하여 1초씩 지연
  }
}

그 결과 첫 번째 checkCount("jeffrey")가 먼저 10번을 출력하고 끝나면 checkCount("oh")가 10번을 출력한다.

jeffrey 0
jeffrey 1
jeffrey 2
jeffrey 3
jeffrey 4
jeffrey 5
jeffrey 6
jeffrey 7
jeffrey 8
jeffrey 9
oh 0
oh 1
oh 2
oh 3
oh 4
oh 5
oh 6
oh 7
oh 8
oh 9

함수 2번 실행하는데 무려 20초가 필요하다. 전혀 도움이 되지 않는다. 이럴 때 사용하는게 goroutines 이다. 사용법은 아주 간단하다. 그저 실행할 함수 앞에 go만 추가해주면 된다.

func main() {
  go checkCount("jeffrey")
  checkCount("oh")
}

그러면 jeffrey를 출력하는 것과 oh를 출력하는게 동시에 실행되어 10초면 2개의 함수가 종료된다.
결과 콘솔에는 다음처럼 나온다.

oh 0
jeffrey 0
jeffrey 1
oh 1
oh 2
jeffrey 2
jeffrey 3
oh 3
jeffrey 4
oh 4
oh 5
jeffrey 5
oh 6
jeffrey 6
oh 7
jeffrey 7
jeffrey 8
oh 8
jeffrey 9
oh 9

동시에 실행되고 있다는 점이 확실히 보인다. 그러면 2개다 go를 붙이면 어떻게 될까 ?

func main() {
  go checkCount("jeffrey")
  go checkCount("oh")
}

둘 다 goroutines를 실행했기 때문에 main함수는 다음으로 실행될 코드가 없어서 종료가 된다. 이 때, 실행 중이던 goroutines들도 자동적으로 종료된다. main이 기다려주지 않는다.
그러면 main 함수와 goroutines가 서로 커뮤니케이션을 하려면 어떻게 해야할까 ?
정답은 Channels 이다.


Channels

우리는 앞서 다뤘듯이 main 함수가 종료되면 진행중이던 goroutines가 종료된다는 것을 확인했다.

func main() {
  people := [2]string{"jeffrey", "oh"}
  for _, person := range people {
    go isHuman(person)
  }
}

func isHuman(person string) bool {
  time.Sleep(time.Second * 5)
  return true
}

위 소스는 실행은 되지만 반응이 없다. 하지만 isHuman 이라는 함수는 true 라는 논리값을 리턴한다. 그렇다고 다음처럼 할 수는 없다.

result := go isHuman(person) // 틀린 소스

Channels을 만드는 방법은 쉽다. 다음처럼 make 함수를 이용하여 chan을 선언하면된다.

c := make(chan bool) // isHuman 이 bool을 return

Channels가 추가된 소스는 다음과 같다.

func main() {
  c := make(chan bool)
  people := [2]string{"jeffrey", "oh"}
  for _, person := range people {
    go isHuman(person, c)
  }
}

func isHuman(person string, c chan bool) {
  time.Sleep(time.Second * 5)
  c <- true // 채널을 이용하여 결과를 알려줌
}

하지만 결과를 출력하면 main 함수가 끝나버려서 출력되지 않는다. 그렇다고 for문 아래에 시간을 10초지연시킨다고 출력되지 않는다. 그러면 메시지는 보내진게 아닌가 ? 라는 의문이 들텐데 메시지는 확실히 보내졌다. 그것을 알아보기 위해 다음 소스를 추가한다.

func main() {
  c := make(chan bool)
  people := [2]string{"jeffrey", "oh"}
  for _, person := range people {
    go isHuman(person, c)
  }
  // 추가
  result := <-c
  fmt.Println(result)
}

func isHuman(person string, c chan bool) {
  time.Sleep(time.Second * 5)
  c <- true // 채널을 이용하여 결과를 알려줌
}

그 결과 true가 출력될 것이다. main 함수는 channels로 부터 결과를 받아야하는 경우에는 결과가 나올 때까지 기다렸다가 다음을 실행한다. 그러므로 위 소스는 main함수가 실행 도중 result := <- c 부분에서 잠시 멈춰있다.

2번이 실행되니 2개의 결과를 받으려면 미리 result에 값을 받지 않고 다음 처럼 하면된다.

// result := <-c // 이 부분을
fmt.Println(<-c)
fmt.Println(<-c)

그 결과 true가 2번이 출력될 것이고 누가 먼저 끝났는지 보기 위해 체크해보려면

func isHuman(person string, c chan bool) {
  time.Sleep(time.Second * 5)
  fmt.Println(person) // 이 부분을 추가하여 체크
  c <- true // 채널을 이용하여 결과를 알려줌
}

위 처럼 변경해보면 알 수 있을 것이다. 그러면 fmt.Println(<-c)가 2번이니까 3번도 되는거 아니냐고 의문이 생길 수 있는데 3번을 출력하면 데드락이 발생한다.

fatal error: all goroutines are asleep - deadlock!

라는 메시지가 출력되면서 에러가 발생한다. 그러니 2번 출력했다고 true가 2번나온게 아니라는 개념이다.


<-c 처럼 메시지를 기다리는 것을 blocking operation이라고 한다.

우리가 받을 수 있는 메시지가 2개라는 것을 확실히 안다면 다음처럼 확인해 볼 수 있다.

func main() {
    c := make(chan string) // type string으로 수정
    people := [2]string{"jeffrey", "oh"}
    for _, person := range people {
        go isHuman(person, c)
    }
    fmt.Println("Waiting for messages")
    resultOne := <-c
    resultTwo := <-c
    fmt.Println("Received this message:", resultOne)
    fmt.Println("Received this message:", resultTwo)
}

func isHuman(person string, c chan string) { // type string으로 수정
    time.Sleep(time.Second * 2)
    c <- person + " is awesome"
}

논리값이 아닌 문자열을 찍어보기위해 약간 수정하였다. 근데 저렇게 하면 많은 값을 받을 땐 역시 힘들다. 따라서 받을 메시지도 반복문을 이용하여 찍어본다.

func main() {
    c := make(chan string) // type string으로 수정
    people := [2]string{"jeffrey", "oh"}
    for _, person := range people {
        go isHuman(person, c)
    }

    for i := 0; i< len(people); i++ {
    fmt.Println(<-c)
    }
}

func isHuman(person string, c chan string) { // type string으로 수정
    time.Sleep(time.Second * 2)
    c <- person + " is awesome"
}

추가적으로 말하자면 channels는 보내기와 받는게 가능한데 보내기만 가능하게끔 설정하려면 다음처럼 하면된다.

func isHuman(person string, c chan<- string) { // chan 에 <- 추가하면
    time.Sleep(time.Second * 2)
    // fmt.Println(<-c) // 여기서는 메시지를 받을 수 없다
    c <- person + " is awesome" // 보내는 것만 가능
}

Rule

goroutineschannels를 사용할 땐 약간의 룰을 지키도록 하자.

  1. main 함수가 끝나기 전에 실행되어야한다.
  2. 받을 데이터, 채널로 보낼 데이터에 대해 어떤 타입인지 구체적으로 지정한다.