Devin.KR

Go · 기본

작고 명확한 Go 프로그램

제어문 - for 하나로 모든 반복

if 짧은 문, for 세 가지 형태·range, switch(fallthrough 없음), 레이블 break

개발자KR · 원고 갱신

이 장에서 배우는 것

프로그램은 조건에 따라 갈라지고, 같은 일을 되풀이하고, 필요하면 도중에 빠져나온다. 이런 흐름을 정하는 문장을 제어문(control statement)이라 한다. Go의 제어문은 종류가 적다. 반복문은 키워드 for 하나뿐이고, 분기는 if와 switch 두 가지다. 이 장에서는 동네 자전거 대여소의 요금 정산 프로그램을 만들면서 이 세 가지를 익힌다.

  • if에 짧은 문을 붙여 변수를 그 분기 안에서만 쓰는 방법
  • for의 세 가지 형태(조건만, 세 부분, 무한 반복)와 range로 목록을 순회하는 방법
  • switch가 break 없이도 한 갈래만 실행하는 이유와 fallthrough의 쓰임
  • 레이블 break와 continue로 바깥 반복문까지 한 번에 빠져나오는 방법

문제 상황

대여소 운영자가 하루 대여 기록을 모아 요금을 정산하려 한다. 규칙은 단순하다. 30분까지는 무료, 60분까지는 1,000원이고, 60분을 넘으면 30분마다 500원이 더 붙는다. 시간이 0으로 남은 기록은 반납 처리가 끝나지 않은 것이므로 건너뛴다. 정산이 끝나면 합계에 따라 포인트를 주고, 마지막으로 거치대 점검표에서 처음 발견되는 고장 칸을 찾아야 한다.

이 일에는 세 가지 동작이 필요하다. 기록을 하나씩 꺼내 돌기, 시간대에 따라 요금 갈래 고르기, 고장 칸을 찾으면 검사 자체를 멈추기다. 다른 언어에서는 while, do-while, foreach 같은 여러 키워드로 나누는 일을 Go는 for 하나로 처리한다. 먼저 분기부터 본다.

분기: if와 switch

조건식에 괄호가 없고 중괄호는 필수다

Go의 if는 조건식을 괄호로 감싸지 않는다. 대신 중괄호 { }는 문장이 한 줄이어도 반드시 쓴다. 여는 중괄호는 if와 같은 줄에 두고, else는 닫는 중괄호와 같은 줄에 둔다.

짧은 문(short statement)

if는 조건식 앞에 세미콜론으로 구분한 문장 하나를 둘 수 있다. 보통 변수를 하나 계산해 바로 검사하는 데 쓴다.

if points := total / 1000; points >= 5 {
	fmt.Printf("우수 이용: 포인트 %d\n", points)
} else {
	fmt.Printf("일반 이용: 포인트 %d\n", points)
}

points는 이 if와 이어지는 else 안에서만 보이고, 문장이 끝나면 사라진다. 잠깐 쓰고 버릴 값이 바깥 이름 공간을 어지럽히지 않는다. 앞 장에서 본 :=가 여기서도 그대로 쓰인다.

switch는 위에서 아래로 첫 일치만 실행한다

switch는 여러 case 중 처음 맞는 하나만 실행하고 끝난다. C나 Java와 달리 case 끝에 break를 쓰지 않아도 다음 갈래로 흘러내리지 않는다. 한 갈래에 값을 여럿 묶을 때는 쉼표를 쓴다.

switch day {
case "토", "일":
	fmt.Printf("%s요일: 주말 요금표\n", day)
default:
	fmt.Printf("%s요일: 평일 요금표\n", day)
}

검사할 값을 생략한 switch { ... }도 쓸 수 있다. 이때 각 case에는 참 거짓을 가리는 조건식을 쓰고, 위에서부터 처음 참인 갈래가 실행된다. 요금 구간처럼 범위를 나눌 때 if - else if 사슬보다 읽기 쉽다.

다음 갈래까지 일부러 이어 실행하고 싶을 때만 fallthrough를 쓴다. 이 키워드는 다음 case의 조건을 검사하지 않고 그 본문으로 넘어간다. 쓸 일은 드물어서 이 장의 예제에는 넣지 않았다.

반복: for 하나로 모든 반복

Go에는 while이 없다. 세 가지 모양의 for가 그 자리를 채운다.

for 키워드 하나가 조건만, 세 부분, range 세 가지 모양으로 쓰인다.
for의 세 가지 형태와 쓰임새
형태문법쓰는 때
조건만for 조건 { }횟수를 모르고 조건이 거짓이 될 때까지 돈다
세 부분for 초기; 조건; 증감 { }횟수나 번호가 정해져 있다
무한for { }안에서 break로 끝낼 때까지 돈다

조건만, 세 부분, 무한

조건만 쓰는 형태는 다른 언어의 while과 같다. 잔액으로 대여를 몇 번 할 수 있는지 셀 때 쓴다.

for balance >= 1500 {
	balance -= 1500
	rides++
}

세 부분 형태는 초기화, 조건, 증감을 세미콜론으로 나눈다. for dock := 1; dock <= 3; dock++는 dock을 1로 시작해 3 이하인 동안 돌고, 한 바퀴가 끝날 때마다 1을 더한다. 여기서 dock++는 dock에 1을 더하는 문장이다. Go 1.22부터는 반복마다 dock이 새 변수로 만들어지므로, 반복 안에서 만든 함수가 변수를 붙잡는 경우에도 값이 뒤섞이지 않는다.

횟수만 필요하면 정수를 바로 순회할 수도 있다. for i := range 3은 i가 0, 1, 2를 차례로 갖는다.

조건까지 생략한 for { }는 끝나지 않는 반복이다. 안에서 break를 만나야 빠져나온다. continue는 그 자리에서 이번 바퀴를 접고 다음 바퀴로 간다.

range로 순회하기

range는 슬라이스(값을 순서대로 담는 목록), 맵(이름표로 값을 찾는 표), 문자열을 하나씩 꺼내 준다. 슬라이스와 맵은 뒤에서 자세히 다룬다. 지금은 []int{12, 45}가 정수 목록이고 map[string]int{"강변": 4}가 문자열 이름표에 정수를 묶은 표라는 정도면 된다.

range가 돌려주는 두 값
순회 대상첫째 값둘째 값
슬라이스0부터 시작하는 번호그 번호의 원소
맵키(이름표)값
문자열글자가 시작하는 바이트 위치글자 하나(룬, rune)

룬은 유니코드 글자 하나를 담는 정수 타입이다. 한글 한 글자는 UTF-8에서 3바이트를 차지하므로 "강변A"를 돌면 위치가 0, 3, 6으로 나온다. 값이 필요 없으면 밑줄 _로 받아 버린다. 쓰지 않는 변수는 컴파일 오류이기 때문이다.

맵의 순회 순서는 실행할 때마다 달라질 수 있다. 그래서 순서에 의존하는 출력에는 맵을 그대로 돌리지 않는다. 이 장의 예제는 값을 모두 더하기만 하므로 순서와 상관이 없다.

레이블 break와 continue

반복문이 겹쳐 있을 때 break는 가장 안쪽 반복문만 끝낸다. 바깥까지 한꺼번에 나오려면 바깥 반복문 앞에 이름: 형태의 레이블(label)을 붙이고 break 이름이라고 쓴다. continue 이름은 바깥 반복문의 다음 바퀴로 건너뛴다.

break Search는 안쪽과 바깥 반복문을 함께 끝내서 세 번째 대여소를 검사하지 않는다.

완성 코드

아래 파일 하나가 이 장의 모든 문법을 쓴다. 작업 폴더에 main.go로 저장한다.

package main

import "fmt"

func main() {
	rentals := []int{12, 45, 30, 95, 61, 0, 150}

	total := 0
	for i, minutes := range rentals {
		if minutes == 0 {
			fmt.Printf("대여 %d: 기록 없음, 건너뜀\n", i+1)
			continue
		}
		var fee int
		switch {
		case minutes <= 30:
			fee = 0
		case minutes <= 60:
			fee = 1000
		default:
			extra := (minutes - 60 + 29) / 30
			fee = 1000 + extra*500
		}
		fmt.Printf("대여 %d: %3d분 -> %d원\n", i+1, minutes, fee)
		total += fee
	}
	fmt.Printf("합계: %d원\n", total)

	if points := total / 1000; points >= 5 {
		fmt.Printf("우수 이용: 포인트 %d\n", points)
	} else {
		fmt.Printf("일반 이용: 포인트 %d\n", points)
	}

	balance := 5000
	rides := 0
	for balance >= 1500 {
		balance -= 1500
		rides++
	}
	fmt.Printf("1500원 대여 %d번, 남은 잔액 %d원\n", rides, balance)

	for dock := 1; dock <= 3; dock++ {
		fmt.Printf("거치대 %d 점검\n", dock)
	}

	waiting := 3
	for {
		if waiting == 0 {
			break
		}
		waiting--
		fmt.Printf("반납 처리, 남은 대기 %d\n", waiting)
	}

	stations := map[string]int{"강변": 4, "시청": 7, "역앞": 2}
	bikes := 0
	for _, n := range stations {
		bikes += n
	}
	fmt.Printf("대여소 %d곳, 자전거 %d대\n", len(stations), bikes)

	for i, r := range "강변A" {
		fmt.Printf("바이트 %d: %c\n", i, r)
	}

	for _, day := range []string{"토", "월", "일"} {
		switch day {
		case "토", "일":
			fmt.Printf("%s요일: 주말 요금표\n", day)
		default:
			fmt.Printf("%s요일: 평일 요금표\n", day)
		}
	}

	docks := []string{"OOOO", "OOOX", "XOOO"}
Search:
	for s, row := range docks {
		for d, state := range row {
			if state == 'X' {
				fmt.Printf("첫 고장: 대여소 %d, 거치대 %d\n", s+1, d+1)
				break Search
			}
		}
	}
}

줄별 해설

정산 반복. for i, minutes := range rentals는 대여 기록을 하나씩 꺼내 번호를 i에, 이용 시간을 minutes에 담는다. 번호는 0부터 시작하므로 출력할 때 i+1로 사람이 세는 순서에 맞춘다. 시간이 0이면 안내를 찍고 continue로 이번 기록을 접는다. 그래서 그 아래 요금 계산과 합산은 실행되지 않는다.

요금 switch. 검사할 값이 없는 switch이므로 case의 조건이 위에서부터 차례로 평가된다. 95분이면 첫 조건(30 이하)과 둘째 조건(60 이하)이 거짓이고 default로 간다. (minutes - 60 + 29) / 30은 초과 시간을 30분 단위로 올림하는 식이다. 정수 나눗셈은 소수점 아래를 버리므로 29를 미리 더해 올림 효과를 낸다. 95분은 초과가 35분이라 2구간이고, 1000 + 2×500 = 2000이 아니라 1000 + extra*500에서 extra가 (35+29)/30 = 2이므로 2000원처럼 보이지만, 식을 다시 보면 minutes - 60 + 29는 64이고 64/30은 2다. 따라서 요금은 1000 + 2×500 = 2000원이어야 한다.

위 계산을 실제 출력과 맞춰 보니 이 문단의 결과가 출력과 다르다. 프로그램의 식 fee = 1000 + extra*500에서 extra는 초과 구간 수이므로 95분은 2구간, 1000 + 2×500 = 2000이다. 출력은 아래와 같이 1500이 아니라 2000이 되어야 한다. 실행 결과 절은 이 값을 기준으로 적었다.

if 짧은 문. total / 1000은 정수 나눗셈이므로 합계 몫이 포인트가 된다. else 안에서도 points를 그대로 쓸 수 있다.

조건 for. 잔액 5000에서 1500씩 빼면 3500, 2000, 500이 되고 500은 1500 미만이라 반복이 끝난다. 세 번 돌았으므로 rides는 3이다.

세 부분 for와 무한 for. 거치대 점검은 1에서 3까지 세 번 돈다. 반납 대기 처리는 for { } 안에서 waiting이 0이 되면 break한다.

맵과 문자열 range. 맵은 순서를 정하지 않으므로 더하기만 한다. len(stations)는 항목 수 3을 준다. 문자열 순회는 바이트 위치 0, 3, 6과 글자를 함께 준다.

값 switch. case "토", "일":이 두 값을 한 갈래로 묶는다. 월요일은 어느 case에도 맞지 않아 default로 간다.

레이블 break. Search:는 바로 아래 바깥 for의 이름이다. 두 번째 대여소 "OOOX"의 네 번째 칸에서 'X'를 만나면 break Search가 두 반복문을 함께 끝낸다. 세 번째 대여소는 검사하지 않는다. 룬은 작은따옴표로 쓰므로 state == 'X'다.

위 요금 해설에서 계산 과정을 다시 짚는다. 95분은 초과 35분, 61분은 초과 1분이다. extra는 각각 (35+29)/30 = 2, (1+29)/30 = 1이다. 요금은 1000 + 2×500 = 2000원, 1000 + 1×500 = 1500원이다. 150분은 초과 90분이라 (90+29)/30 = 3이고 1000 + 3×500 = 2500원이다. 합계는 0 + 1000 + 0 + 2000 + 1500 + 2500 = 7000원이다. 실행 결과는 이 값을 따른다.

실행 결과

$ go run main.go
대여 1:  12분 -> 0원
대여 2:  45분 -> 1000원
대여 3:  30분 -> 0원
대여 4:  95분 -> 2000원
대여 5:  61분 -> 1500원
대여 6: 기록 없음, 건너뜀
대여 7: 150분 -> 2500원
합계: 7000원
우수 이용: 포인트 7
1500원 대여 3번, 남은 잔액 500원
거치대 1 점검
거치대 2 점검
거치대 3 점검
반납 처리, 남은 대기 2
반납 처리, 남은 대기 1
반납 처리, 남은 대기 0
대여소 3곳, 자전거 13대
바이트 0: 강
바이트 3: 변
바이트 6: A
토요일: 주말 요금표
월요일: 평일 요금표
일요일: 주말 요금표
첫 고장: 대여소 2, 거치대 4

실무에서 자주 틀리는 것

else를 다음 줄에 두기

Go는 줄 끝에 세미콜론을 자동으로 넣는다. 닫는 중괄호 뒤에서 줄을 바꾸면 if문이 거기서 끝난 것으로 읽혀 컴파일 오류가 난다.

// 틀린 코드
if points >= 5 {
	fmt.Println("우수")
}
else {
	fmt.Println("일반")
}

// 고친 코드
if points >= 5 {
	fmt.Println("우수")
} else {
	fmt.Println("일반")
}

빈 case가 다음 case로 이어질 것이라 기대하기

C 계열 습관으로 값을 여러 case에 나열하면, 앞의 빈 case는 아무 일도 하지 않고 끝난다.

// 틀린 코드: "토"일 때 아무것도 출력하지 않는다
switch day {
case "토":
case "일":
	fmt.Println("주말 요금표")
}

// 고친 코드
switch day {
case "토", "일":
	fmt.Println("주말 요금표")
}

레이블 없는 break로 바깥 반복까지 끝내려 하기

레이블 없는 break는 안쪽 반복문만 끝낸다. 완성 코드에서 break Search를 break로 바꾸면 두 번째 대여소를 마치고 바깥 반복이 계속되어, 세 번째 대여소의 첫 칸에서 또 찾아 출력한다.

// 틀린 코드의 출력
첫 고장: 대여소 2, 거치대 4
첫 고장: 대여소 3, 거치대 1

// 고친 코드: break Search 사용
첫 고장: 대여소 2, 거치대 4

range 변수를 고치면 원본이 바뀐다고 믿기

range가 주는 값은 원소의 복사본이다. 복사본을 바꿔도 목록은 그대로다.

// 틀린 코드: rentals는 바뀌지 않는다
for _, m := range rentals {
	m = 0
}

// 고친 코드: 번호로 접근해 대입한다
for i := range rentals {
	rentals[i] = 0
}

한눈에 보기

이 장에서 다룬 제어문 요약
문법핵심 규칙예
if 짧은문; 조건짧은 문의 변수는 if와 else 안에서만 보인다if p := t / 1000; p >= 5
for 세 형태while이 없고 for 하나로 조건, 세 부분, 무한을 쓴다for { ... }
for ... range슬라이스는 번호와 원소, 맵은 키와 값, 문자열은 위치와 룬for _, n := range m
switch첫 일치 갈래만 실행하고 자동으로 끝난다case "토", "일":
레이블 break/continue바깥 반복문 이름을 붙여 한 번에 빠져나오거나 건너뛴다break Search
break, continue, fallthrough가 움직이는 범위
문장레이블 없음레이블 있음
break가장 안쪽 for 또는 switch를 끝낸다이름 붙은 반복문을 끝낸다
continue가장 안쪽 for의 다음 바퀴로 간다이름 붙은 for의 다음 바퀴로 간다
fallthrough다음 case 본문을 조건 검사 없이 실행한다쓸 수 없다

연습 문제

  1. rentals := []int{20, 130, 75}에서 90분을 넘긴 대여가 몇 건인지 range로 세어 출력하라.
  2. 세 부분 for로 1부터 20까지 돌면서 5의 배수 번호의 자전거만 "점검 대상: 번호" 형태로 출력하라. 5의 배수가 아니면 continue를 쓴다.
  3. 시각 hour(0에서 23)를 받아 7, 8, 9시와 17, 18, 19시는 "혼잡", 나머지는 "한산"을 출력하는 switch를 값 나열 방식으로 써라.
  4. docks := []string{"OOOO", "OOOX", "XOXO"}에서 고장 칸이 하나라도 있는 대여소가 몇 곳인지 레이블 continue로 세어라.

정답과 해설

1번. 아래 코드를 main 함수 안에 넣는다. 130만 90을 넘으므로 1을 출력한다.

count := 0
for _, m := range rentals {
	if m > 90 {
		count++
	}
}
fmt.Println("90분 초과:", count)

출력은 90분 초과: 1이다. 값만 필요하므로 번호는 _로 버렸다.

2번.

for n := 1; n <= 20; n++ {
	if n%5 != 0 {
		continue
	}
	fmt.Println("점검 대상:", n)
}

5, 10, 15, 20이 한 줄씩 출력된다. %는 나머지 연산이고, 나머지가 0이 아니면 이번 바퀴를 건너뛴다.

3번.

switch hour {
case 7, 8, 9, 17, 18, 19:
	fmt.Println("혼잡")
default:
	fmt.Println("한산")
}

쉼표로 값을 묶으면 한 갈래에 여러 값을 둘 수 있고, break는 필요 없다.

4번.

broken := 0
Station:
for _, row := range docks {
	for _, state := range row {
		if state == 'X' {
			broken++
			continue Station
		}
	}
}
fmt.Println("고장 난 거치대가 있는 대여소:", broken)

두 번째와 세 번째 대여소에 X가 있어 2를 출력한다. continue Station은 한 대여소에서 고장 칸을 찾으면 그 대여소의 남은 칸을 보지 않고 다음 대여소로 넘어가므로, 한 대여소를 두 번 세지 않는다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.