주의: go 1.22 부터 range loop 변수의 스코프가 변경되어 이 글의 내용은 사실이 아니게 되었습니다. 다음 링크를 참조하세요.
https://go.dev/blog/loopvar-preview
slice, map 등의 컬렉션을 편리한 문법으로 순회할 수 있도록 하는 기능이다.
//iterating slice
a := []string{"Foo", "Bar"}
//0 Foo
//1 Bar
for i, s := range a {
fmt.Println(i, s)
}
//iterating map
m := map[string]int{
"Foo": 1,
"Bar": 2,
"Baz": 3,
}
//Foo 1
//Bar 2
//Baz 3
for key, val := range m {
fmt.Printf("key: %s, val: %d\n", key, val)
}
다음의 goroutine을 이용한 코드를 보자.
arr := []string{"Foo", "Bar", "Baz"}
wg := sync.WaitGroup{}
for i, elem := range arr {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, elem)
}()
}
wg.Wait()
https://go.dev/play/p/-OYbE-fzHHr
코드가 의도한 바는 명확하다. 루프를 돌면서 slice 안의 원소를 하나씩 출력하는 것이다.
2 Baz
2 Baz
2 Baz
그러나 실제로 실행해보면 놀라운 일이 일어난다. 세 번의 출력 함수가 모두 배열의 마지막 값을 출력한다.
앞서 소개한 예제는 다음의 코드와 똑같이 동작한다. 매번 새로운 변수를 생성하는 것이 아니라, 기존에 선언한 변수의 값을 바꾼다.
arr := []string{"Foo", "Bar", "Baz"}
wg := sync.WaitGroup{}
i := 0
elem := arr[0]
for {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, elem)
}()
i += 1
if i == 3 {
break
}
elem = arr[i]
}
wg.Wait()
https://go.dev/play/p/Cztw7wIUCzd
문법만 보면 마치 매번 새로운 변수를 선언하는 것처럼 보이기 때문에 매우 헷갈리는 특성이다. 기존 작성된 코드와 하위 호환을 유지하기 위해 go1.x 에서는 바뀔 예정이 없다고 한다.
closure에서 함수 바깥의 변수를 참조 할 때는 값을 복사하지 않고 레퍼런스를 참조한다. 다음의 코드를 실행하면 5가 출력된다.
i := 0
func() {
i = 5
}()
fmt.Println(i)
https://go.dev/play/p/8LyyfxM3uZu
두 가지 원인을 알고 나면 마지막 값을 출력하게 된 이유를 짐작할 수 있다.
fmt.Println
을 실행하기도 전에 마지막 루프까지 모두 실행해버렸다배열의 크기를 10000으로 늘려서 실행해보면 무작위로 아무 값이나 들어가는 것을 관찰할 수 있다. fmt.Println()
이 실행될 당시의 변수 값을 참조하는 것인데 결과가 매번 바뀌며 예측이 거의 불가능하다.
package main
import (
"fmt"
"sync"
)
func main() {
arr := []string{}
for i := 0; i < 10000; i++ {
if i%2 == 0 {
arr = append(arr, "Foo")
continue
}
arr = append(arr, "Bar")
}
wg := sync.WaitGroup{}
for i, elem := range arr {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, elem)
}()
}
wg.Wait()
}
https://go.dev/play/p/IQ_sCS46UrN
11 Bar
27 Bar
90 Foo
11 Bar
11 Bar
11 Bar
11 Bar
11 Bar
11 Bar
299 Bar
11 Bar
27 Bar
43 Bar
539 Bar
43 Bar
683 Bar
...
loop 안에서 매번 새로운 변수를 생성하여 참조하는 방법이다.
package main
import (
"fmt"
"sync"
)
func main() {
arr := []string{"Foo", "Bar", "Baz"}
wg := sync.WaitGroup{}
for i, elem := range arr {
wg.Add(1)
j, e := i, elem
go func() {
defer wg.Done()
fmt.Println(j, e)
}()
}
wg.Wait()
}
https://go.dev/play/p/o6oBTCmIkvJ
closure에서 참조하는 대신, 익명 함수의 인자로 넘기면 값이 복사되어 쓰인다.
package main
import (
"fmt"
"sync"
)
func main() {
arr := []string{"Foo", "Bar", "Baz"}
wg := sync.WaitGroup{}
for i, elem := range arr {
wg.Add(1)
go func(i int, elem string) {
defer wg.Done()
fmt.Println(i, elem)
}(i, elem)
}
wg.Wait()
}