Menu

Golang range: 모든 형태, 내놓는 값, 함정

for range가 슬라이스, 배열, 문자열, 맵, 채널, 정수, 이터레이터 함수에 대해 내놓는 값, 값 변수가 복사본인 이유, 순회 중에 슬라이스나 맵을 수정하면 일어나는 일, 그리고 Go 1.23의 함수에 대한 range를 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

range는 컬렉션을 훑는 for의 한 형태입니다. 반복마다 값을 하나나 둘 주며, 그 값이 무엇인지는 순회하는 타입에 따라 다릅니다:

피연산자첫 번째 값두 번째 값참고
슬라이스 []T, 배열 [n]T인덱스 int요소 T, 복사본
배열 포인터 *[n]T인덱스요소
문자열바이트 인덱스 intruneUTF-8 디코딩
map[K]VKV무작위 순서
채널 chan T요소 T없음채널이 닫힐 때까지
정수 n0부터 n-1까지없음Go 1.22
func(yield func() bool)없음없음Go 1.23
func(yield func(V) bool)V없음Go 1.23
func(yield func(K, V) bool)KVGo 1.23

받을 변수 고르기

for i, v := range s {}  // both
for i := range s {}     // index (or key) only
for _, v := range s {}  // value only
for range s {}          // neither: just repeat len(s) times

사용하지 않는 변수를 선언하면 컴파일 오류이므로, 필요 없는 것은 빼거나 빈 식별자로 두세요. for range 3 {}은 무언가를 세 번 반복하는 가장 짧은 방법입니다.

값은 복사본이다

반복마다 요소가 값 변수로 복사됩니다. 변수를 바꿔도 컬렉션은 바뀌지 않습니다:

요소가 큰 구조체라면 복사에 시간도 듭니다. 큰 구조체의 슬라이스라면 for i := range ss[i]로 복사를 피하세요.

문자열은 룬을 내놓는다

문자열을 순회하면 UTF-8이 디코딩됩니다. 첫 번째 값은 각 문자가 시작하는 바이트 오프셋이므로 숫자를 건너뛸 수 있습니다:

문자 대신 바이트를 훑으려면 인덱스 반복문 for i := 0; i < len(s); i++를 쓰거나 []byte(s)를 순회하세요. UTF-8의 세부 사항은 rune과 byte 페이지를 참고하세요.

맵: 무작위 순서

맵의 순회 순서는 정해져 있지 않고, 코드가 특정 순서에 의존하지 못하도록 런타임이 무작위로 섞습니다. 결정적인 출력이 필요하면 키를 정렬하세요:

순회하면서 항목을 삭제하는 것은 안전하며, 아직 도달하지 않은 삭제된 항목은 나오지 않습니다. 항목을 추가하는 것은 허용되지만 예측할 수 없습니다. 새 키가 같은 반복문에서 나중에 나올 수도 있고 안 나올 수도 있습니다. fmt.Println은 맵을 키 순서로 정렬해 출력하므로 마지막 줄은 안정적입니다.

채널: 닫힐 때까지

채널을 순회하면 채널이 닫히고 비워질 때까지 값을 받습니다. 아무도 닫지 않으면 반복문은 영원히 블록됩니다(모든 고루틴이 블록되면 런타임이 fatal error: all goroutines are asleep - deadlock!으로 멈춥니다):

채널은 받는 쪽이 아니라 보내는 쪽이 닫습니다. 버퍼 있는 채널과 select는 채널 페이지를 참고하세요.

정수 (Go 1.22)

range n은 0부터 n 직전까지 셉니다. 반복 변수의 타입은 n의 타입입니다:

for i := range 3 {}         // i is int: 0, 1, 2
for i := range uint8(3) {}  // i is uint8
for range 0 {}              // runs zero times

n이 음수여도 0번 실행됩니다. n은 반복문이 시작되기 전에 한 번 평가됩니다.

range가 무엇을 언제 평가하는가

range 뒤의 표현식은 첫 번째 반복 전에 한 번 평가됩니다. 슬라이스라면 range는 그 슬라이스 헤더를 쓰므로 길이가 고정됩니다:

반복문 길이가 3으로 고정되었으므로 추가된 99는 방문되지 않습니다. 하지만 nums[2] = 30은 보입니다. 그 시점에 nums가 순회 중인 슬라이스와 아직 기반 배열을 공유하기 때문입니다. 두 줄의 순서를 바꾸면 30도 사라집니다. append가 용량 3을 넘어서 새 배열을 할당하고, 쓰기가 그 새 배열에 들어가기 때문입니다. 배열 값을 순회하면 먼저 배열 전체를 복사하므로 arr[2]의 변경은 보이지 않습니다. 복사를 피하려면 &arrarr[:]를 순회하세요.

함수에 대한 range (Go 1.23)

Go 1.23부터 range는 이터레이터 함수도 받습니다. 이터레이터는 yield 콜백을 받아서 값마다 한 번씩 호출하고, yieldfalse를 반환하면(반복문 본문이 breakreturn을 실행할 때 그렇게 됩니다) 멈춥니다:

iter.Seq[V]iter.Seq2[K, V]는 두 가지 이터레이터 모양의 표준 이름입니다. 표준 라이브러리는 slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All에서, 그리고 Go 1.24에서는 strings.SplitSeq, strings.Lines, bytes.SplitSeq에서 이를 반환합니다. 이터레이터는 yield의 반환 값을 존중해야 합니다. yieldfalse를 반환한 뒤 다시 호출하면 패닉이 납니다.

반복 변수는 반복마다 새로 생긴다 (Go 1.22)

Go 1.22부터 for ... range가 선언하는 변수는 반복마다 새것입니다. 클로저나 고루틴에서 v를 캡처하면 결국 마지막 요소를 담게 되는 공유 변수가 아니라 그 반복의 값을 캡처합니다. Go 1.22 이전에는 반복문 안에 v := v가 필요했지만 이제는 불필요합니다. 규칙은 go.modgo 버전으로 정해지므로 오래된 모듈은 예전 동작을 유지합니다.

흔한 실수

  • 값 변수의 주소를 얻음. Go 1.22 이전에는 ptrs = append(ptrs, &v)가 매번 같은 주소를 저장했습니다. 1.22부터는 서로 다른 주소를 저장하지만, 각 주소는 슬라이스 요소가 아니라 복사본을 가리킵니다. 슬라이스 안을 가리키려면 &items[i]를 쓰세요.
  • 맵 순서를 기대함. 작은 테스트에서 정렬된 것처럼 보이는 출력도 계속 정렬되어 있지는 않습니다.
  • 아무도 닫지 않는 채널을 순회함. 반복문과 그것을 실행하는 고루틴이 영원히 기다립니다.
  • 큰 배열을 값으로 순회함. 먼저 배열 전체가 복사됩니다. 대신 그 배열의 슬라이스를 순회하세요.

자주 묻는 질문

Go에서 range는 무엇을 반환하나요?

피연산자에 따라 다릅니다. 슬라이스나 배열은 인덱스와 요소, 문자열은 바이트 인덱스와 룬, 맵은 키와 값, 채널은 수신한 각 값, 정수 n(Go 1.22)은 0부터 n-1까지, 이터레이터 함수(Go 1.23)는 그 함수가 yield하는 값입니다. 두 번째 변수는 생략할 수 있고, 첫 번째 변수는 _로 버릴 수 있습니다.

range 값을 수정해도 슬라이스가 바뀌지 않는 이유는 무엇인가요?

값 변수는 요소의 복사본입니다. for _, v := range items { v.Price = 0 }는 복사본만 바꿉니다. 대신 인덱스를 쓰세요: for i := range items { items[i].Price = 0 }. 또는 포인터 슬라이스를 순회하세요.

Go에서 정수에 대해 range를 쓸 수 있나요?

네, Go 1.22부터 가능합니다. for i := range 5 { ... }i가 0부터 4까지 실행됩니다. for range 5 { ... }는 변수 없이 다섯 번 반복합니다. 모듈의 go.modgo 1.22 이상이 적혀 있어야 합니다.

맵을 순회하면서 삭제해도 안전한가요?

네. 아직 도달하지 않은 항목을 삭제하면 그 항목은 나오지 않고, 현재 항목을 삭제하는 것도 괜찮습니다. 반복 중에 항목을 추가하는 것도 허용되지만, 새 항목이 방문될지는 알 수 없으므로 그것에 의존하지 마세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기