- Go 1.22는
for 루프 변수를 루프 전체가 아니라 반복별 범위로 바꿔, 클로저가 같은 변수를 잘못 캡처하는 Go의 대표적 실수를 줄이려 함
- 기존 의미론에서는 goroutine이 없어도 반복 이후 실행되는 함수가 같은
v나 i를 참조해, 마지막 값만 보거나 테스트가 잘못 통과할 수 있음
go vet과 gopls의 loopclosure 분석기는 확실한 경우만 잡아 미탐이 생기고, 더 공격적인 검사기는 오탐 때문에 불필요한 x:= x 코드를 늘릴 수 있음
- 새 의미론은
go.mod에 go 1.22 이상을 선언한 모듈에만 적용되며, Go 1.21에서는 GOEXPERIMENT=loopvar로 미리보기를 실행할 수 있음
- Google은 2023년 5월 초부터 내부 Go 툴체인에서 이 모드를 모든 빌드에 강제했고, 4개월 동안 프로덕션 문제 보고는 없었지만 잘못 작성된 테스트는 드러났음
기존 for 루프의 변수 캡처 함정
- Go의 기존
for 루프 변수는 루프 전체 범위를 가지므로, 반복이 끝난 뒤 그 변수를 참조하는 코드가 의도와 다른 값을 볼 수 있음
values := []string{"a", "b", "c"}를 순회하며 세 goroutine을 만들면, 각 goroutine이 반복별 v가 아니라 같은 변수 v를 출력함
- 동시성 없이도 같은 문제가 생김
- 반복 안에서
func() { fmt.Println(i) }를 슬라이스에 저장한 뒤 나중에 실행하면, 각 함수가 반복별 값이 아니라 같은 i를 참조함
프로덕션 장애와 분석기의 한계
- 이런 실수는 여러 회사의 프로덕션 문제로 이어졌고, Let’s Encrypt의 공개 이슈도 그중 하나임
- Let’s Encrypt 사례에서는 map 순회 중
k는 kCopy := k로 복사했지만, modelToAuthzPB(&v)가 결과 생성 과정에서 v의 필드 포인터를 사용해 v도 별도로 복사해야 했음
- 변수 캡처가 여러 함수에 걸쳐 있어 문제를 알아차리기 어려웠음
- 정적 분석 도구는 변수가 반복 이후까지 살아남는지 판단하기 어려워 오탐과 미탐 사이에서 타협해야 함
go vet과 gopls의 loopclosure 분석기는 확실한 문제만 보고해 미탐을 감수함
- 더 공격적인 검사기는 올바른 코드까지 잘못된 코드로 지목할 수 있음
- 오픈소스 Go 코드에서
x := x 줄을 추가한 커밋을 살펴보면, 실제 버그 수정뿐 아니라 불필요한 변경도 많이 섞여 있었음
- 개발자가 검사기를 만족시키려고 불필요한 코드를 추가하는 상황이 있었음
informer := informer와 a := a 같은 두 diff 중 하나만 버그 수정이고 다른 하나는 불필요한 변경이었지만, 타입과 함수 정보를 모르면 구분하기 어려움
Go 1.22의 새 루프 의미론
- Go 1.22에서는
for 루프 변수가 반복마다 별도 범위를 갖도록 바뀔 예정임
- 앞선 예시들은 더 이상 버그가 있는 Go 프로그램이 아니게 되고, 이런 실수로 생기는 프로덕션 문제와 부정확한 검사 도구의 필요도 줄어듦
- 하위 호환성을 위해 새 의미론은
go.mod에 go 1.22 이상을 선언한 모듈의 패키지에만 적용됨
- 코드베이스 전체를 한 번에 바꾸지 않고 점진적으로 이동할 수 있음
//go:build 줄로 파일 단위 제어도 가능함
- 기존 코드는 현재와 같은 의미를 그대로 유지함
- 수정은 새 코드나 업데이트된 코드에만 적용됨
- 특정 패키지에서 의미론이 바뀌는 시점을 개발자가 제어할 수 있음
이전 Go 버전에서의 안전장치
- Go의 forward compatibility 작업에 따라 Go 1.21은
go 1.22 이상을 선언한 코드를 컴파일하지 않음
- Go 1.20.8과 Go 1.19.13 포인트 릴리스에도 같은 효과를 내는 특수 처리가 들어감
- Go 1.22가 릴리스된 뒤 새 의미론에 의존해 작성된 코드는, 매우 오래된 지원 종료 Go 버전을 쓰지 않는 한 기존 의미론으로 컴파일되지 않음
Go 1.21에서 미리보기 실행하기
- Go 1.21에는 루프 범위 변경의 미리보기가 포함됨
GOEXPERIMENT=loopvar를 설정해 컴파일하면 go.mod의 go 줄을 무시하고 모든 루프에 새 의미론이 적용됨
- 패키지와 모든 의존성이 새 루프 의미론에서도 테스트를 통과하는지 확인하려면 다음처럼 실행함
GOEXPERIMENT=loopvar go test
- Go Playground에서는 프로그램 맨 위에
// GOEXPERIMENT=loopvar 주석을 넣어 새 의미론을 시험할 수 있음
- Google 내부 Go 툴체인은 2023년 5월 초부터 모든 빌드에서 이 모드를 강제하도록 패치됐고, 이후 4개월 동안 프로덕션 코드 문제 보고는 없었음
새 의미론이 드러내는 테스트 버그
- 새 루프 의미론은 프로덕션 코드 문제는 일으키지 않았지만, 잘못 통과하던 테스트를 드러냈음
t.Parallel을 쓰는 서브테스트 예시에서 Go 1.21은 전체 루프가 끝날 때까지 각 서브테스트를 막은 뒤 병렬 실행함
- 루프가 끝나면
v는 항상 6이므로 모든 서브테스트가 6이 짝수인지 확인하고 통과함
- 실제 테스트 케이스에는
1이 있으므로 테스트는 실패해야 함
- Go 1.21에서는
loopclosure 분석기의 정밀도가 개선되어 이 문제를 식별하고 보고할 수 있음
- Go Playground 보고 예시: 프로그램 예시
go vet이 테스트에서 이런 문제를 보고하면, 이를 고치는 것이 Go 1.22 준비에 도움이 됨
- 새 의미론 적용 시 특정 테스트 실패를 일으키는 루프를 찾는 도구와 예시는 FAQ에 정리되어 있음
더 읽을거리