[Go] Goroutine in For loop

Falcon·2021년 10월 11일
2

go

목록 보기
2/11
post-thumbnail

🎯 Goal

For loop 내에서 goroutine 을 쓸 때 유의사항을 알아보자.

예제

웹 페이지를 방문하는 함수

// send only channel parameter
// response code가 2XX 이 아니면 error 반환.
func hitUrl(url string, channel chan<- responseResult) error {
	response, error := http.Get(url)
	// send 전용인지 receive 전용인지 명확하게 명시.

	if error != nil {
		return errRequestFailed
	}
	// send data to channel
	channel <- responseResult{url: url, statusCode: response.StatusCode}

	if isSuccess(response.StatusCode) {
		return nil
	} else {
		return errInvalidRequest
	}
}

❌ 틀린 예제 (feat. IIFE)

main.go

func main() {

		channel := make(chan responseResult)

		urls := []string{
			"https://www.airbnb.com",
			"https://www.google.com",
			"https://www.naver.com/",
			"https://www.facebook.com/",
		}


		// some problem occurs!
	        // ❌❌❌ 여기가 문제다 ❌❌❌
		for index, url := range urls {
			//waitGroup.Add(1)
			fmt.Printf("index : %d, url: %s\n", index, url)
			go func() {
				// 여기로 url 이 왜 다 안넘어가지?
				err := hitUrl(url, channel)
				if err != nil {
					log.Fatalln(err)
				}
			}()
			fmt.Printf("22222 index : %d, url: %s\n", index, url)
		}


		// 여기서는 range-based for loop 보다
		// 호출 횟수를 여러번하는게 좋음.
		// 호출 순서 상관없이 channel Message Queue (FIFO Queue) 에 먼저 추가된 메시지부터 아무거나 받아오기 때문
		for i:= 0; i < len(urls); i++ {
			fmt.Printf("result: %v\n", <-channel)
		}

}

for loop 안에서 문제가 발생한다.
for loop 이 다 돌고 나서야 IIFE (Immediately Invoked Function Expression) 이 호출된다.

출력 결과

index : 0, url: https://www.airbnb.com
22222 index : 0, url: https://www.airbnb.com
index : 1, url: https://www.google.com
22222 index : 1, url: https://www.google.com
index : 2, url: https://www.naver.com/
22222 index : 2, url: https://www.naver.com/
index : 3, url: https://www.facebook.com/
22222 index : 3, url: https://www.facebook.com/

result: {https://www.facebook.com/ 200} // 맨 마지막 Loop 의 facebook 만 출력된다.
result: {https://www.facebook.com/ 200}
result: {https://www.facebook.com/ 200}
result: {https://www.facebook.com/ 200}

🤔 왜 이런 현상이 나타나는 걸까?

range-based for loop 에서는 사실
내부적으로는 모든 대상 객체의 elements를 복사해 놓고 index, value 를 매 루프마다 임시로 할당한다.

아래 코드는

for index, value := range array {
// do something
}

내부적에서 다음과 같이 동작한다.

// 일단 임시 객체를 복사함.
len_temp := len(range)
range_temp := range
for index_temp = 0; index_temp < len_temp; index_temp++ {
     value_temp = range_temp[index_temp]
     index = index_temp
     value = value_temp
// do something
}

👉 우리가 지정한 for loop 속 goroutine 이 실제로 실행되기 전에 loop 을 다 돈 상태에서 원소 값을 참조하면서 임시 value 값 (마지막 원소의 복제본)만 반복해서 얻어오게 되는 것이다.

아래 코드를 추가하여 for loop 을 일부러 지연시켜서 돌리면 실제로 정상 값이 출력된다.

for index, url := range urls {
    fmt.Printf("index : %d, url: %s\n", index, url)
    go func() {
      fmt.Println("index in goroutine : ", index)
      err := hitUrl(url, channel)
    // 중략..
    }()

// 이렇게 지체시켜서 loop 을 돌리면 정상출력된다.
// 그치만 실행 속도가 지연되므로 구데기 방법이다.
   time.Sleep(time.Millisecond * 300)
   fmt.Printf("22222 index : %d, url: %s\n", index, url)
 }

해결 방법

IIFE사용시 local variable parameter 를 호출시에 넘긴다.

✅ 올바른 사용


          // goroutine!
for index, url := range urls {
       fmt.Printf("index : %d, url: %s\n", index, url)
       go func(url string) {
       // ✅ loop 내에서 현재 사용되는 temp_value (url) 을 넘긴다.
           err := hitUrl(url, channel)
           if err != nil {
               log.Fatalln(err)
             }
       }(url)
}

출력결과

index : 0, url: https://www.airbnb.com
index : 1, url: https://www.google.com
index : 2, url: https://www.naver.com/
index : 3, url: https://www.facebook.com/

result: {https://www.airbnb.com 200} // 요청이 완료된 것부터 정상 출력
result: {https://www.naver.com/ 200}
result: {https://www.google.com 200}
result: {https://www.facebook.com/ 200}

📝 결론

  • channel Messsage 를 받아올 때는 index 순서에 종속적이지 않도록 호출 횟수지정하자. (range-based for loop 사용 X)
  • for loop 내에서 goroutine 사용시 IIFE에 Argument 를 직접 넘기자.

🔗 Reference

Some tricks and tips for using for range in Golang

profile
I'm still hungry

2개의 댓글

comment-user-thumbnail
2022년 3월 16일

Go 처음사용하면서 방금전까지 틀린 예제와 같은 유형의 코드를 작성했어요
똑같은 데이터만 전달되고있어서 해결방법을 찾다가 누군가 나와 같은 고민을 하고있겠지 싶어서 검색했는데..
덕분에 잘 해결했습니다. 정말 감사합니다 ㅎㅎ

1개의 답글