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 조건 { } | 횟수를 모르고 조건이 거짓이 될 때까지 돈다 |
| 세 부분 | 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}가 문자열 이름표에 정수를 묶은 표라는 정도면 된다.
| 순회 대상 | 첫째 값 | 둘째 값 |
|---|---|---|
| 슬라이스 | 0부터 시작하는 번호 | 그 번호의 원소 |
| 맵 | 키(이름표) | 값 |
| 문자열 | 글자가 시작하는 바이트 위치 | 글자 하나(룬, rune) |
룬은 유니코드 글자 하나를 담는 정수 타입이다. 한글 한 글자는 UTF-8에서 3바이트를 차지하므로 "강변A"를 돌면 위치가 0, 3, 6으로 나온다. 값이 필요 없으면 밑줄 _로 받아 버린다. 쓰지 않는 변수는 컴파일 오류이기 때문이다.
맵의 순회 순서는 실행할 때마다 달라질 수 있다. 그래서 순서에 의존하는 출력에는 맵을 그대로 돌리지 않는다. 이 장의 예제는 값을 모두 더하기만 하므로 순서와 상관이 없다.
레이블 break와 continue
반복문이 겹쳐 있을 때 break는 가장 안쪽 반복문만 끝낸다. 바깥까지 한꺼번에 나오려면 바깥 반복문 앞에 이름: 형태의 레이블(label)을 붙이고 break 이름이라고 쓴다. continue 이름은 바깥 반복문의 다음 바퀴로 건너뛴다.
완성 코드
아래 파일 하나가 이 장의 모든 문법을 쓴다. 작업 폴더에 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 | 가장 안쪽 for 또는 switch를 끝낸다 | 이름 붙은 반복문을 끝낸다 |
continue | 가장 안쪽 for의 다음 바퀴로 간다 | 이름 붙은 for의 다음 바퀴로 간다 |
fallthrough | 다음 case 본문을 조건 검사 없이 실행한다 | 쓸 수 없다 |
연습 문제
rentals := []int{20, 130, 75}에서 90분을 넘긴 대여가 몇 건인지range로 세어 출력하라.- 세 부분
for로 1부터 20까지 돌면서 5의 배수 번호의 자전거만 "점검 대상: 번호" 형태로 출력하라. 5의 배수가 아니면continue를 쓴다. - 시각
hour(0에서 23)를 받아 7, 8, 9시와 17, 18, 19시는 "혼잡", 나머지는 "한산"을 출력하는switch를 값 나열 방식으로 써라. 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
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.