반복문
반복문이란?
반복문은 특정 코드 블록을 여러 번 실행할 때 사용합니다. 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++ 처럼 길이를 미리 저장할 필요 없습니다.