Go 1.22에서 추가된 for loop 변수 스코프 변경

문제 상황

레거시 배치 작업을 리팩토링하던 중 Go 1.22 업그레이드 이슈를 겪었다. 기존 코드는 goroutine으로 병렬 처리를 하는데, 간헐적으로 같은 값이 중복 처리되는 버그가 있었다.

for _, item := range items {
    go func() {
        process(item) // 모든 goroutine이 마지막 item 처리
    }()
}

기존엔 loop 변수가 반복마다 재사용되어서 클로저에서 참조하면 마지막 값만 잡혔다. 그래서 명시적으로 변수를 복사해야 했다.

for _, item := range items {
    item := item // 이렇게 복사
    go func() {
        process(item)
    }()
}

Go 1.22 변경사항

Go 1.22부터는 loop 변수가 매 반복마다 새로 생성된다. 더 이상 item := item 같은 workaround가 필요없다.

for _, item := range items {
    go func() {
        process(item) // 정상 동작
    }()
}

마이그레이션 주의사항

기존 코드에서 의도적으로 loop 변수 주소를 사용한 경우 동작이 달라질 수 있다. 우리 코드베이스에서는 발견되지 않았지만, 테스트 커버리지가 낮은 부분은 주의가 필요하다.

go vet이 문제될 만한 패턴을 찾아주지만, 완벽하진 않았다. 배포 전 충분한 회귀 테스트가 필수다.

결론

10년 가까이 Go 개발자들을 괴롭혔던 gotcha가 드디어 해결됐다. 코드가 직관적으로 동작하게 되어서 신규 개발자 온보딩할 때 설명할 게 하나 줄었다.