Updated:

6 minute read

개요

Go 언어는 반복문으로 for 키워드만 제공하며, 다양한 형태로 사용할 수 있습니다.

기본 for 문

1. 전통적인 C 스타일 for 문

package main

import "fmt"

func main() {
    // 기본 for 문
    for i := 0; i < 10; i++ {
        if i == 2 {
            continue // 2는 건너뜀
        }
        if i == 8 {
            break // 8에서 종료
        }
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 출력: 0 1 3 4 5 6 7
}

2. 조건문만 있는 for 문 (while 문 대체)

func whileStyle() {
    count := 0
    for count < 10 {
        fmt.Print(count, " ")
        count++
    }
    fmt.Println()
    // 출력: 0 1 2 3 4 5 6 7 8 9
}

3. 무한 루프

func infiniteLoop() {
    count := 0
    for {
        if count >= 5 {
            break
        }
        fmt.Print(count, " ")
        count++
    }
    fmt.Println()
    // 출력: 0 1 2 3 4
}

range를 사용한 반복

1. 슬라이스/배열 순회

func rangeSlice() {
    fruits := []string{"apple", "banana", "cherry"}
    
    // 인덱스와 값 모두 사용
    for index, value := range fruits {
        fmt.Printf("[%d] %s\n", index, value)
    }
    
    // 값만 사용
    for _, value := range fruits {
        fmt.Println(value)
    }
    
    // 인덱스만 사용
    for index := range fruits {
        fmt.Println(index)
    }
}

2. 맵(Map) 순회

func rangeMap() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
    }
    
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
    
    // 키만 필요한 경우
    for name := range scores {
        fmt.Println(name)
    }
}

3. 문자열 순회

func rangeString() {
    str := "Hello, 世界"
    
    // rune (유니코드 코드 포인트) 단위로 순회
    for index, runeValue := range str {
        fmt.Printf("%d: %c (%U)\n", index, runeValue, runeValue)
    }
    
    // 바이트 단위로 순회
    for i := 0; i < len(str); i++ {
        fmt.Printf("%d: %c\n", i, str[i])
    }
}

4. 채널(Channel) 순회

func rangeChannel() {
    ch := make(chan int)
    
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch) // 반드시 채널을 닫아야 range가 종료됨
    }()
    
    for value := range ch {
        fmt.Print(value, " ")
    }
    fmt.Println()
    // 출력: 0 1 2 3 4
}

중첩 루프와 라벨

라벨을 사용한 중첩 루프 제어

func nestedLoopWithLabel() {
OuterLoop:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break OuterLoop // 외부 루프까지 종료
            }
            fmt.Printf("(%d,%d) ", i, j)
        }
    }
    fmt.Println()
    // 출력: (0,0) (0,1) (0,2) (1,0)
}

func continueWithLabel() {
OuterLoop:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if j == 1 {
                continue OuterLoop // 외부 루프의 다음 반복으로
            }
            fmt.Printf("(%d,%d) ", i, j)
        }
    }
    fmt.Println()
    // 출력: (0,0) (1,0) (2,0)
}

고급 활용 및 패턴

1. 역순 순회

func reverseIteration() {
    nums := []int{1, 2, 3, 4, 5}
    
    for i := len(nums) - 1; i >= 0; i-- {
        fmt.Print(nums[i], " ")
    }
    fmt.Println()
    // 출력: 5 4 3 2 1
}

2. 슬라이스 필터링

func filterSlice() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    var evens []int
    
    for _, num := range numbers {
        if num%2 == 0 {
            evens = append(evens, num)
        }
    }
    
    fmt.Println(evens) // 출력: [2 4 6 8 10]
}

3. 동시 순회 (zip 패턴)

func zipIteration() {
    names := []string{"Alice", "Bob", "Carol"}
    ages := []int{25, 30, 28}
    
    // 더 짧은 슬라이스 길이까지만 순회
    minLen := len(names)
    if len(ages) < minLen {
        minLen = len(ages)
    }
    
    for i := 0; i < minLen; i++ {
        fmt.Printf("%s is %d years old\n", names[i], ages[i])
    }
}

성능 최적화 팁

1. 슬라이스 길이 캐싱

// 비효율적
for i := 0; i < len(slice); i++ {
    // len(slice)가 매 반복마다 호출됨
}

// 효율적
length := len(slice)
for i := 0; i < length; i++ {
    // 길이를 한 번만 계산
}

// range는 내부적으로 최적화되어 있음 (권장)
for i := range slice {
    // 가장 효율적
}

2. range 복사 주의

type LargeStruct struct {
    data [1000]int
}

func rangeCopy() {
    items := []LargeStruct{{}, {}, {}}
    
    // 비효율적: 각 반복마다 구조체 복사 발생
    for _, item := range items {
        _ = item
    }
    
    // 효율적: 인덱스로 접근
    for i := range items {
        _ = items[i]
    }
    
    // 또는 포인터 슬라이스 사용
    itemPtrs := []*LargeStruct{{}, {}, {}}
    for _, item := range itemPtrs {
        _ = item // 포인터만 복사됨
    }
}

일반적인 실수와 주의사항

1. range 변수 재사용 문제 (Go 1.21 이전)

// Go 1.21 이전: 주의 필요
func rangeVariableIssue() {
    values := []int{1, 2, 3}
    var funcs []func()
    
    for _, v := range values {
        // 잘못된 방법 (Go 1.21 이전)
        funcs = append(funcs, func() {
            fmt.Print(v, " ") // 모두 같은 v를 참조
        })
    }
    
    for _, f := range funcs {
        f() // 출력: 3 3 3 (예상: 1 2 3)
    }
    fmt.Println()
    
    // 올바른 방법 (Go 1.21 이전)
    funcs = nil
    for _, v := range values {
        v := v // 새 변수에 복사
        funcs = append(funcs, func() {
            fmt.Print(v, " ")
        })
    }
    
    for _, f := range funcs {
        f() // 출력: 1 2 3
    }
    fmt.Println()
}

// Go 1.22+: 자동으로 해결됨
// 각 반복마다 새로운 변수가 생성됨

2. 맵 순회 시 동시 수정 불가

func mapModificationDuringIteration() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    
    // 위험: 순회 중 맵 수정
    for key := range m {
        if key == "b" {
            delete(m, key) // 동작하지만 예측 불가능
        }
    }
    
    // 안전한 방법: 삭제할 키 수집 후 처리
    toDelete := []string{}
    for key := range m {
        if key == "b" {
            toDelete = append(toDelete, key)
        }
    }
    for _, key := range toDelete {
        delete(m, key)
    }
}

3. 슬라이스 순회 중 수정

func sliceModificationDuringIteration() {
    numbers := []int{1, 2, 3, 4, 5}
    
    // range는 초기 슬라이스의 복사본을 순회
    for i, v := range numbers {
        numbers[i] = v * 2 // OK
        fmt.Print(numbers[i], " ")
    }
    fmt.Println()
    // 출력: 2 4 6 8 10
    
    // 주의: append로 확장해도 range는 원래 길이만 순회
    numbers = []int{1, 2, 3}
    for _, v := range numbers {
        if v < 3 {
            numbers = append(numbers, v+10)
        }
        fmt.Print(v, " ")
    }
    fmt.Println()
    fmt.Println(numbers)
    // 순회 출력: 1 2 3
    // 최종 슬라이스: [1 2 3 11 12]
}

실전 예제

예제 1: 2차원 배열 순회

func traverse2DArray() {
    matrix := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    
    for i, row := range matrix {
        for j, val := range row {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
        }
    }
}

예제 2: 조건부 반복 종료

func findElement() {
    numbers := []int{5, 2, 8, 1, 9, 3}
    target := 8
    
    found := false
    for i, num := range numbers {
        if num == target {
            fmt.Printf("Found %d at index %d\n", target, i)
            found = true
            break
        }
    }
    
    if !found {
        fmt.Printf("%d not found\n", target)
    }
}

예제 3: 슬라이스 변환

func transformSlice() {
    words := []string{"hello", "world", "go"}
    
    // 모든 문자열을 대문자로 변환
    for i := range words {
        words[i] = strings.ToUpper(words[i])
    }
    
    fmt.Println(words) // 출력: [HELLO WORLD GO]
}

벤치마크 비교

import "testing"

func BenchmarkForLoop(b *testing.B) {
    data := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        sum := 0
        for j := 0; j < len(data); j++ {
            sum += data[j]
        }
    }
}

func BenchmarkForRange(b *testing.B) {
    data := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        sum := 0
        for _, v := range data {
            sum += v
        }
    }
}

// 일반적으로 range가 더 빠르거나 비슷한 성능을 보임

정리

  • for는 Go의 유일한 반복문 키워드
  • range는 슬라이스, 배열, 맵, 문자열, 채널 순회에 사용
  • 라벨을 사용하여 중첩 루프를 제어 가능
  • Go 1.22부터 range 변수 스코핑이 개선됨
  • 성능을 위해 불필요한 복사를 피하고 range 사용 권장
  • 순회 중 컬렉션 수정 시 주의 필요