본문으로 건너뛰기

반복문

반복문이란?

반복문은 특정 코드 블록을 여러 번 실행할 때 사용합니다. Go의 반복문은 단 하나, for만 존재 합니다. while도, do-while도 없습니다. 하지만 for 하나로 C의 for, while, do-while을 모두 표현할 수 있습니다.

단일 반복 구조는 Go의 "단순함이 최고다" 철학의 산물입니다. 익숙해지면 오히려 더 직관적이라는 것을 알게 됩니다.


for 세 가지 기본 형태

1. C 스타일 for (초기화; 조건; 후처리)

package main

import "fmt"

func main() {
// 기본 C 스타일
for i := 0; i < 5; i++ {
fmt.Printf("i = %d\n", i)
}

// 역순
for i := 4; i >= 0; i-- {
fmt.Printf("역순 i = %d\n", i)
}

// 2씩 증가
for i := 0; i <= 10; i += 2 {
fmt.Printf("짝수: %d\n", i)
}
}

2. while 스타일 (조건만 있는 for)

package main

import "fmt"

func main() {
// while(n > 0) 과 동일
n := 10
for n > 0 {
fmt.Printf("n = %d\n", n)
n /= 2
}

// 콜라츠 수열
x := 27
steps := 0
for x != 1 {
if x%2 == 0 {
x /= 2
} else {
x = 3*x + 1
}
steps++
}
fmt.Printf("콜라츠 수열: 27에서 1까지 %d 단계\n", steps)
}

3. 무한 루프

package main

import (
"fmt"
"math/rand"
)

func main() {
// for {} 는 while(true) 와 동일
count := 0
for {
n := rand.Intn(10)
count++
if n == 7 {
fmt.Printf("%d번째 시도에서 7을 뽑았습니다!\n", count)
break
}
}
}

range 기반 순회

range 는 슬라이스, 맵, 문자열, 채널을 순회하는 Go의 핵심 키워드입니다. 인덱스와 값을 동시에 반환합니다.

슬라이스 순회

package main

import "fmt"

func main() {
fruits := []string{"사과", "바나나", "체리", "딸기"}

// 인덱스와 값 모두 사용
for i, fruit := range fruits {
fmt.Printf("[%d] %s\n", i, fruit)
}

// 값만 사용 (인덱스 무시)
fmt.Println("\n과일 목록:")
for _, fruit := range fruits {
fmt.Println("-", fruit)
}

// 인덱스만 사용
fmt.Println("\n인덱스:")
for i := range fruits {
fmt.Println(i)
}
}

실행 결과:

[0] 사과
[1] 바나나
[2] 체리
[3] 딸기

과일 목록:
- 사과
- 바나나
- 체리
- 딸기

인덱스:
0
1
2
3

맵 순회

package main

import (
"fmt"
"sort"
)

func main() {
scores := map[string]int{
"Alice": 92,
"Bob": 87,
"Carol": 95,
"Dave": 78,
}

// 맵 순회 — 순서 보장 없음
fmt.Println("맵 순회 (순서 임의):")
for name, score := range scores {
fmt.Printf(" %s: %d\n", name, score)
}

// 정렬된 순서로 출력하려면 키를 먼저 정렬
fmt.Println("\n정렬된 순서:")
keys := make([]string, 0, len(scores))
for k := range scores {
keys = append(keys, k)
}
sort.Strings(keys)
for _, name := range keys {
fmt.Printf(" %s: %d\n", name, scores[name])
}
}

문자열 순회 (UTF-8 자동 디코딩)

package main

import "fmt"

func main() {
s := "Go언어"

// range로 순회하면 Unicode 코드포인트(rune) 단위
fmt.Println("rune 순회:")
for i, r := range s {
fmt.Printf(" 인덱스=%d, 문자=%c, 코드포인트=%U\n", i, r, r)
}

fmt.Println("\nbyte 순회:")
for i, b := range []byte(s) {
fmt.Printf(" 인덱스=%d, byte=0x%02X\n", i, b)
}
}

실행 결과:

rune 순회:
인덱스=0, 문자=G, 코드포인트=U+0047
인덱스=1, 문자=o, 코드포인트=U+006F
인덱스=2, 문자=언, 코드포인트=U+C5B8
인덱스=5, 문자=어, 코드포인트=U+C5B4

byte 순회:
인덱스=0, byte=0x47
인덱스=1, byte=0x6F
인덱스=2, byte=0xEC
...

한글은 UTF-8에서 3바이트를 차지하므로, range 문자열 순회 시 인덱스가 3씩 건너뛰는 것을 볼 수 있습니다.

채널 순회

package main

import "fmt"

func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

func main() {
// 채널이 닫힐 때까지 range로 수신
for n := range generate(1, 2, 3, 4, 5) {
fmt.Printf("수신: %d\n", n)
}
}

실행 결과:

수신: 1
수신: 2
수신: 3
수신: 4
수신: 5

break와 continue

package main

import "fmt"

func main() {
// continue — 현재 반복 건너뜀
fmt.Println("홀수만 출력:")
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 짝수는 건너뜀
}
fmt.Printf(" %d\n", i)
}

// break — 루프 즉시 종료
fmt.Println("\n첫 번째 음수 찾기:")
nums := []int{3, 7, 2, -1, 8, -3, 5}
for i, n := range nums {
if n < 0 {
fmt.Printf(" 인덱스 %d에서 첫 음수 발견: %d\n", i, n)
break
}
}
}

실행 결과:

홀수만 출력:
1
3
5
7
9

첫 번째 음수 찾기:
인덱스 3에서 첫 음수 발견: -1

label을 이용한 중첩 루프 탈출

중첩 루프에서 break는 가장 안쪽 루프만 탈출합니다. label 을 사용하면 외부 루프까지 한 번에 탈출하거나 건너뛸 수 있습니다.

package main

import "fmt"

func main() {
// label 없는 break — 안쪽 루프만 탈출
fmt.Println("label 없는 break:")
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break // 안쪽 for만 탈출
}
fmt.Printf(" (%d,%d)\n", i, j)
}
}

// label break — 바깥 루프까지 탈출
fmt.Println("\nlabel break:")
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
fmt.Printf(" (%d,%d)에서 전체 탈출\n", i, j)
break Outer
}
fmt.Printf(" (%d,%d)\n", i, j)
}
}

// label continue — 바깥 루프의 다음 반복으로 이동
fmt.Println("\nlabel continue:")
Loop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue Loop // 바깥 루프의 다음 i로
}
fmt.Printf(" (%d,%d)\n", i, j)
}
}
}

실행 결과:

label 없는 break:
(0,0)
(1,0)
(2,0)

label break:
(0,0)
(0,1)
(0,2)
(1,0)
(1,1)에서 전체 탈출

label continue:
(0,0)
(1,0)
(2,0)

실전 예제: 파일 라인 처리

package main

import (
"bufio"
"fmt"
"strings"
)

// 로그 엔트리 구조체
type LogEntry struct {
Level string
Message string
}

// 로그 파일 파싱 (문자열 리더로 시뮬레이션)
func parseLog(content string) []LogEntry {
var entries []LogEntry
scanner := bufio.NewScanner(strings.NewReader(content))

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

// 빈 줄이나 주석 건너뜀
if line == "" || strings.HasPrefix(line, "#") {
continue
}

// "LEVEL: message" 형식 파싱
parts := strings.SplitN(line, ": ", 2)
if len(parts) != 2 {
fmt.Printf("형식 오류, 건너뜀: %q\n", line)
continue
}

entries = append(entries, LogEntry{
Level: parts[0],
Message: parts[1],
})
}

return entries
}

func filterByLevel(entries []LogEntry, level string) []LogEntry {
var result []LogEntry
for _, e := range entries {
if e.Level == level {
result = append(result, e)
}
}
return result
}

func main() {
logContent := `
# 서버 로그 2024-01-01
INFO: 서버 시작됨
DEBUG: 설정 파일 로드
INFO: 데이터베이스 연결 성공
WARNING: 연결 풀 80% 사용 중
ERROR: 쿼리 타임아웃 발생
INFO: 재시도 성공
잘못된형식
ERROR: 디스크 공간 부족
DEBUG: 캐시 초기화
INFO: 서버 정상 운영 중
`

entries := parseLog(logContent)
fmt.Printf("\n총 %d개 엔트리 파싱됨\n", len(entries))

// 레벨별 필터링
for _, level := range []string{"ERROR", "WARNING", "INFO"} {
filtered := filterByLevel(entries, level)
fmt.Printf("\n[%s] %d건:\n", level, len(filtered))
for _, e := range filtered {
fmt.Printf(" - %s\n", e.Message)
}
}
}

실행 결과:

형식 오류, 건너뜀: "잘못된형식"

총 9개 엔트리 파싱됨

[ERROR] 2건:
- 쿼리 타임아웃 발생
- 디스크 공간 부족

[WARNING] 1건:
- 연결 풀 80% 사용 중

[INFO] 4건:
- 서버 시작됨
- 데이터베이스 연결 성공
- 재시도 성공
- 서버 정상 운영 중

실전 예제: 동시성 워커 패턴

package main

import (
"fmt"
"sync"
"time"
)

type Job struct {
ID int
Value int
}

type Result struct {
JobID int
Output int
}

// 워커 함수 — for range 채널로 작업을 지속 수신
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { // 채널이 닫힐 때까지 반복
// 작업 처리 시뮬레이션
time.Sleep(time.Millisecond * 10)
output := job.Value * job.Value // 제곱 계산
fmt.Printf(" 워커 %d: Job %d 처리 완료 (%d² = %d)\n",
id, job.ID, job.Value, output)
results <- Result{JobID: job.ID, Output: output}
}
}

func main() {
const numWorkers = 3
const numJobs = 9

jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)

var wg sync.WaitGroup

// 워커 고루틴 시작
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}

// 작업 전송
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Value: j}
}
close(jobs) // 더 이상 작업 없음을 워커에게 알림

// 모든 워커 종료 후 results 채널 닫기
go func() {
wg.Wait()
close(results)
}()

// 결과 수집
var total int
for r := range results {
total += r.Output
}
fmt.Printf("\n1²+2²+...+9² = %d\n", total)
}

고수 팁

1. range는 복사본을 순회한다

슬라이스 요소를 수정하려면 인덱스로 접근해야 합니다.

nums := []int{1, 2, 3, 4, 5}

// 잘못된 예 — v는 복사본, 원본이 수정되지 않음
for _, v := range nums {
v *= 2 // 효과 없음
}

// 올바른 예 — 인덱스로 직접 접근
for i := range nums {
nums[i] *= 2
}
fmt.Println(nums) // [2 4 6 8 10]

2. label은 goto 대신 사용하라

Go에는 goto가 있지만 사용을 피하세요. 중첩 루프 탈출에는 label break/continue가 훨씬 명확합니다.

3. 무한 루프 패턴 — 이벤트 루프

for {
select {
case msg := <-msgChan:
handleMessage(msg)
case <-done:
return // 종료 신호
}
}

4. 슬라이스 길이 캐싱은 불필요

Go 컴파일러는 슬라이스 길이를 자동으로 최적화합니다. n := len(s); for i := 0; i < n; i++ 처럼 길이를 미리 저장할 필요 없습니다.