for 문
for 문 (For statements)
반복 실행은 Go에서 거의 매일 만나는 일이에요. 반복 횟수를 정해 두고 돌릴 수도 있고, 조건이 참인 동안 돌릴 수도 있고, 배열이나 맵 같은 것들을 처음부터 끝까지 훑을 수도 있죠. Go의 for 문은 이 모든 반복을 딱 하나의 키워드로 처리해요. 다른 언어에서 while이나 foreach를 따로 기억해야 하는 것과 달리, Go는 for 하나로 세 가지 형태를 지원해요.
출처: Go Specification
본문
for 문은 블록의 실행을 반복하도록 지정해요. 반복의 제어 방식에 따라 세 가지 형태가 있어요. 반복은 단일 조건으로 제어되거나, **for 절(ForClause)**로 제어되거나, **range 절(RangeClause)**로 제어될 수 있어요.
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block . Condition = Expression .
단일 조건을 쓰는 for 문
가장 단순한 형태의 for 문은 boolean 조건이 true로 평가되는 동안 블록을 반복 실행해요. 조건은 각 반복이 시작되기 전에 평가되고요. 조건이 생략되면 true와 같다고 취급해요.
for a < b {
a *= 2
}
for 절을 쓰는 for 문
ForClause를 쓰는 for 문도 역시 조건에 의해 제어되지만, 여기에 초기화(init) 문과 후처리(post) 문을 추가로 지정할 수 있어요. 대입문이나 증가·감소 문이 여기 들어가죠. init 문은 짧은 변수 선언일 수 있지만, post 문은 그러면 안 돼요.
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] . InitStmt = SimpleStmt . PostStmt = SimpleStmt .
for i := 0; i < 10; i++ {
f(i)
}
init 문이 비어 있지 않으면, 첫 번째 반복의 조건을 평가하기 전에 한 번 실행돼요. post 문은 블록이 매번 실행된 뒤에 실행되는데, 블록이 실제로 실행됐을 때만 실행돼요. ForClause의 각 요소는 비워 둘 수 있지만, 조건만 쓰는 경우가 아니라면 세미콜론은 반드시 필요해요. 조건이 생략되면 true와 같다고 취급해요.
for cond { S() } is the same as for ; cond ; { S() }
for { S() } is the same as for true { S() }
여기서 꼭 짚고 넘어갈 부분이 있어요. 각 반복은 자기만의 별도 선언 변수를 가져요 Go 1.22. 첫 번째 반복이 쓰는 변수는 init 문이 선언하고, 이후 각 반복이 쓰는 변수는 post 문을 실행하기 전에 암묵적으로 선언돼서, 그 시점의 이전 반복 변수 값으로 초기화돼요.
var prints []func()
for i := 0; i < 5; i++ {
prints = append(prints, func() { println(i) })
i++
}
for _, p := range prints {
p()
}
이 코드는
1
3
5
을 출력해요. 반복마다 i가 새로 선언되니까, 이전 반복의 값을 기억하고 있다가 클로저가 그 고유한 값을 참조하게 되죠. Go 1.22 이전에는 반복들이 자기만의 변수를 갖지 않고 하나의 변수 집합을 공유했어요. 그 경우 위 예제는
6
6
6
을 출력하죠.
range 절을 쓰는 for 문
range 절을 쓰는 for 문은 배열(array), 슬라이스(slice), 문자열(string), 맵(map)의 모든 항목을 훑거나, 채널(channel)에서 수신되는 값들을 돌거나, 0부터 어떤 상한까지의 정수 값들(Go 1.22)을 돌거나, Go 1.23부터는 이터레이터 함수의 yield 함수로 전달되는 값들을 돌아요. 각 항목마다 **반복 값(iteration values)**을 해당 **반복 변수(iteration variables)**에 (있으면) 대입하고 블록을 실행해요.
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
range 절 오른쪽의 표현식을 **range 표현식(range expression)**이라고 불러요. 배열, 배열 포인터, 슬라이스, 문자열, 맵, 수신 연산을 허용하는 채널, 정수, 또는 특정 시그니처를 가진 함수(아래 참고)가 될 수 있어요. 대입과 마찬가지로, 왼쪽 피연산자가 있으면 그것들은 주소 지정 가능(addressable)하거나 맵 인덱스 표현식이어야 하고, 반복 변수를 나타내요. range 표현식이 함수라면 반복 변수의 최대 개수는 함수 시그니처에 따라 달라져요. range 표현식이 채널이나 정수라면 반복 변수는 최대 하나만 허용되고, 그 외에는 최대 둘까지 허용돼요. 마지막 반복 변수가 빈 식별자라면, range 절은 그 식별자가 없는 같은 절과 동일해요.
range 표현식 x는 루프를 시작하기 전에 평가돼요. 예외가 하나 있는데, 반복 변수가 최대 하나이고 x 또는 len(x)가 상수(constant)라면 range 표현식은 평가되지 않아요.
왼쪽의 함수 호출은 반복마다 한 번씩 평가돼요. 각 반복에서, 해당 반복 변수들이 존재한다면 반복 값이 다음과 같이 생성돼요.
Range expression 1st value 2nd value array or slice a [n]E, *[n]E, or []E index i int a[i] E string s string type index i int see below rune map m map[K]V key k K m[k] V channel c chan E, <-chan E element e E integer value n integer type, or untyped int value i see below function, 0 values f func(yield func() bool) function, 1 value f func(yield func(V) bool) value v V yield cannot be variadic function, 2 values f func(yield func(K, V) bool) key k K v V yield cannot be variadic
-
배열, 배열 포인터, 슬라이스 값
a에 대해, 인덱스 반복 값은 요소 인덱스 0에서 시작해 증가하는 순서로 생성돼요. 반복 변수가 최대 하나 있으면, range 루프는0부터len(a)-1까지의 반복 값을 만들고 배열·슬라이스 자체에는 인덱싱하지 않아요.nil슬라이스라면 반복 횟수는 0이에요. -
문자열 값에 대해,
range절은 바이트 인덱스 0에서 시작해 문자열의 유니코드 코드 포인트들을 훑어요. 반복이 진행될수록 인덱스 값은 문자열 안의 연속된 UTF-8 인코딩 코드 포인트의 첫 바이트 인덱스가 되고, 두 번째 값은 타입이rune인 해당 코드 포인트 값이 돼요. 반복 중 유효하지 않은 UTF-8 시퀀스를 만나면 두 번째 값은 유니코드 대체 문자인0xFFFD가 되고, 다음 반복은 문자열에서 한 바이트만 전진해요. -
맵에 대한 반복 순서는 명시되지 않았고, 한 반복에서 다음 반복으로 같다고 보장되지 않아요. 반복 중에 아직 도달하지 않은 맵 항목이 제거되면 해당 반복 값은 생성되지 않아요. 반복 중에 맵 항목이 생성되면 그 항목은 반복 중 생성될 수도 있고 건너뛰어질 수도 있어요. 그 선택은 생성된 항목마다, 그리고 반복마다 달라질 수 있어요. 맵이
nil이라면 반복 횟수는 0이에요. -
채널에 대해, 생성되는 반복 값은 채널이 닫힐(closed) 때까지 채널로 보내진 연속적인 값들이에요. 채널이
nil이라면 range 표현식은 영원히 블록돼요. -
정수 값
n에 대해 — 여기서n은 정수 타입이거나 타입이 없는(untyped) 정수 상수예요 —0부터n-1까지의 반복 값이 증가하는 순서로 생성돼요.n이 정수 타입이면 반복 값도 같은 타입이에요. 그렇지 않으면n의 타입은 반복 변수에 대입된 것처럼 결정돼요. 구체적으로 말하면, 반복 변수가 이미 존재하면 반복 값의 타입은 그 반복 변수의 타입인데, 그것은 정수 타입이어야 해요. 그 외에, 반복 변수가range절에 의해 선언되거나 없으면, 반복 값의 타입은n의 기본 타입(default type)이에요.n<= 0이면 루프는 반복을 전혀 실행하지 않아요. -
함수
f에 대해, 반복은f를 인자로 하여 새로 합성된yield함수를 호출함으로써 진행돼요.f가 반환되기 전에yield가 호출되면,yield의 인자들이 루프 본문을 한 번 실행하기 위한 반복 값이 돼요. 각 연속 루프 반복 후에yield는true를 반환하고, 루프를 계속하기 위해 다시 호출될 수 있어요. 루프 본문이 종료되지 않는 한,range절은f가 반환될 때까지 각yield호출에 대해 이런 방식으로 반복 값을 계속 생성해요. 루프 본문이 (break문으로든) 종료되면yield는false를 반환하고 다시 호출되면 안 돼요.
range 표현식의 타입이 타입 매개변수(type parameter)라면, 그 타입 집합의 모든 타입은 같은 기반 타입(underlying type)을 가져야 하고 range 표현식이 그 타입에 대해 유효해야 해요. 또는 타입 집합이 채널 타입을 포함한다면, 동일한 요소 타입을 가진 채널 타입만 포함해야 하고, 모든 채널 타입이 수신 연산을 허용해야 해요.
반복 변수는 range 절에 의해 짧은 변수 선언(:=) 형태로 선언될 수 있어요. 이 경우 그들의 스코프는 for 문의 블록이고, 각 반복은 자기만의 새 변수를 가져요 Go 1.22 (참고: ForClause를 쓰는 for 문). 변수들은 각각의 반복 값 타입을 가져요.
반복 변수가 range 절에 의해 명시적으로 선언되지 않았다면, 그것들은 이미 존재해야 해요. 이 경우 반복 값들은 대입 문(assignment statement)에서처럼 각 변수에 대입돼요.
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
f(i)
}
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}
var key string
var val interface{} // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]
var ch chan Work = producer()
for w := range ch {
doWork(w)
}
// empty a channel
for range ch {}
// call f(0), f(1), ... f(9)
for i := range 10 {
// type of i is int (default type for untyped constant 10)
f(i)
}
// invalid: 256 cannot be assigned to uint8
var u uint8
for u = range 256 {
}
// invalid: 1e3 is a floating-point constant
for range 1e3 {
}
// fibo generates the Fibonacci sequence
fibo := func(yield func(x int) bool) {
f0, f1 := 0, 1
for yield(f0) {
f0, f1 = f1, f0+f1
}
}
// print the Fibonacci numbers below 1000:
for x := range fibo {
if x >= 1000 {
break
}
fmt.Printf("%d ", x)
}
// output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
// iteration support for a recursive tree data structure
type Tree[K cmp.Ordered, V any] struct {
left, right *Tree[K, V]
key K
value V
}
func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool {
return t == nil || t.left.walk(yield) && yield(t.key, t.value) && t.right.walk(yield)
}
func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
t.walk(yield)
}
// walk tree t in-order
var t Tree[string, int]
for k, v := range t.Walk {
// process k, v
}
// xor returns the xor-ed bytes of S
func xor[S ~[]byte](s S) byte {
var r byte
for _, b := range s {
r ^= b
}
return r
}
더 알아보기
range절의 이터레이터 함수(Go 1.23)에 대해 더 깊이 보려면 Go 1.23 릴리스 노트를 살펴보면 좋아요.- 반복 변수가 반복마다 새로 선언되는 변화는 Go 1.22 변경 사항에서 확인할 수 있어요.
for문 안에서 제어 흐름을 바꾸는break,continue,goto문도 Go Specification의 문(Statements) 부분에서 함께 보면 이해가 이어져요.