프로그래밍Go 개발자

Go에서 for-init 후크를 사용한 루프는 어떻게 작동하며, 루프 변수의 범위 특성이 고루틴 및 클로저에서 복잡한 버그를 유발할 수 있는 이유는 무엇인가요?

Hintsage AI 어시스턴트로 면접 통과

답변.

Go에서 for 루프 구조는 초기화 블록(init), 조건 검사, 후치 표현식이 포함될 수 있습니다. 역사적으로 이러한 메커니즘은 코드 작성을 용이하게 하고 C 유사 언어의 관습을 따르기 위해 만들어졌습니다. 그러나 Go에서는 루프 변수(i)의 범위 특성이 중첩 함수, 클로저 및 고루틴 내에서의 동작에 큰 영향을 미칩니다.

문제 — 고루틴이나 클로저를 각 반복에서 실행할 때 의도치 않은 동작이 발생하는 경우가 많습니다: 변수 i는 복사되지 않고 "참조로 캡처"되며, 즉 클로저가 루프의 공통 변수에 접근하게 되어 기댓값과 다르게 마지막 반복의 값을 가지게 됩니다. 이는 모든 고루틴/클로저에서 동일한 결과를 초래하지만, 로직은 그렇지 않을 것이라고 가정할 수 있습니다.

해결책 — 각 반복의 변수를 전달할 필요가 있을 경우, 명시적으로 변수를 복사(추가 변수를 통해)하거나 클로저의 인수로 전달하세요.

코드 예시:

for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // 올바름! 복사된 값 } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // 오류: 모든 고루틴이 3을 출력함 }

주요 특징:

  • for 루프에서 루프 변수는 for 블록의 범위 내에서 암묵적으로 선언됩니다.
  • 클로저/고루틴에서 루프 변수를 캡처하면 모든 인스턴스에서 변수 간 "공유"가 발생합니다.
  • 각 반복에서 변수를 새로운 변수로 복사하여 우회할 수 있습니다.

속임수 질문들.

break 또는 continue 사용 시 for 변수의 범위가 변경되나요?

아니요. for에서 선언된 변수의 범위는 항상 해당 루프 블록으로 제한됩니다. break 또는 continue는 단지 현재 반복을 중단하지만 변수를 밖으로 "전파"하지 않습니다.

for의 init 부분에 선언된 변수를 루프 외부의 메서드에서 캡처할 수 있나요?

아니요. 변수는 오직 for 내부와 해당 블록의 모든 중첩에서만 보이며 루프가 끝난 후에는 그 외부에서 보이지 않습니다.

if 변수를 defer 표현식에서 캡처하면 어떻게 되나요?

상황은 동일합니다: defer 함수는 생성 시점의 값이 아니라 defer 실행 시점의 현재 변수 값을 "보게 됩니다" (보통 마지막 루프 값).

for i := 0; i < 3; i++ { defer fmt.Println(i) // 모든 defer가 3을 출력함 }

일반적인 오류 및 안티 패턴

  • 새로운 변수에 복사하지 않고 루프 변수를 캡처함
  • 명시적으로 전달하지 않고 익명 함수에 루프 변수를 전달함 (지연 바인딩 효과)
  • 변수가 범위에 대한 고려 없이 루프 내에서 defer 사용

실제 사례

부정적인 케이스

Go 웹 서버에서 개발자는 여러 고루틴을 다양한 포트를 처리하기 위해 실행했으며 포트 인덱스를 루프 변수로 사용하고 람다 표현식에서 이를 캡처했습니다. 모든 고루틴은 배열의 마지막 포트를 참조했습니다.

장점:

  • 간단하고 "명시적인" 루프 구현

단점:

  • 잘못된 동작 논리
  • 긴 시간 동안 해결되지 않은 버그

긍정적인 케이스

팀 내에서는 루프 변수의 값을 항상 새로운 변수에 복사하여 클로저/고루틴이 이를 캡처하도록 하는 규칙을 도입했습니다.

장점:

  • 예상치 못한 부작용이 없음
  • 코드의 투명성

단점:

  • "마이크로 최적화"가 사라짐 (스택에 추가 변수가 생김, 하지만 큰 문제는 아님)