본문으로 건너뛰기

조건문

조건문이란?

조건문은 특정 조건에 따라 프로그램의 실행 흐름을 분기시키는 제어 구조입니다. Go의 조건문은 다른 언어와 비슷하지만 몇 가지 중요한 차이점이 있습니다. 가장 눈에 띄는 특징은 조건식에 괄호를 쓰지 않는다 는 점과, 중괄호({})는 항상 필수 라는 점입니다.

Go의 설계 철학은 "명확하고 단순하게"입니다. 조건문도 이 철학을 그대로 반영합니다. 불필요한 괄호를 없애고, 모호한 문법을 허용하지 않아 코드를 읽기 쉽게 만듭니다.


if-else 기본 문법

가장 기본적인 형태부터 살펴봅니다.

package main

import "fmt"

func main() {
score := 85

// 기본 if
if score >= 60 {
fmt.Println("합격")
}

// if-else
if score >= 60 {
fmt.Println("합격")
} else {
fmt.Println("불합격")
}

// if-else if-else 체인
if score >= 90 {
fmt.Println("A등급")
} else if score >= 80 {
fmt.Println("B등급")
} else if score >= 70 {
fmt.Println("C등급")
} else if score >= 60 {
fmt.Println("D등급")
} else {
fmt.Println("F등급")
}
}

실행 결과:

합격
합격
B등급

한 가지 주의사항: elseelse if는 반드시 닫는 중괄호 }같은 줄 에 위치해야 합니다. 다음 줄에 쓰면 컴파일 에러가 납니다.

// 컴파일 에러 — else가 다음 줄에 있음
if score >= 60 {
fmt.Println("합격")
}
else { // ← 에러!
fmt.Println("불합격")
}

초기화 구문이 포함된 if

Go의 if문에는 초기화 구문(init statement) 을 넣을 수 있습니다. 세미콜론(;)으로 초기화와 조건을 구분합니다.

if 초기화구문; 조건식 {
// 본문
}

이 구문의 핵심은 초기화 구문에서 선언한 변수의 스코프가 if 블록 안으로 제한 된다는 점입니다. 불필요한 변수가 함수 스코프로 흘러나오지 않아 코드가 깔끔해집니다.

package main

import (
"errors"
"fmt"
"strconv"
)

func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, errors.New("유효한 나이가 아닙니다: " + s)
}
if age < 0 || age > 150 {
return 0, errors.New("나이 범위를 벗어났습니다")
}
return age, nil
}

func main() {
// 초기화 구문 포함 if — err는 if 블록 안에서만 유효
if age, err := parseAge("25"); err != nil {
fmt.Println("에러:", err)
} else {
fmt.Printf("나이: %d세\n", age)
}

// err 변수는 여기서 접근 불가 (스코프 제한)

// 나쁜 케이스
if age, err := parseAge("abc"); err != nil {
fmt.Println("에러:", err)
} else {
fmt.Printf("나이: %d세\n", age)
}
}

실행 결과:

나이: 25세
에러: 유효한 나이가 아닙니다: abc

초기화 구문은 Go 에러 처리의 핵심 패턴입니다. if err := doSomething(); err != nil 형태를 수도 없이 마주치게 됩니다.


중첩 if vs Early Return 패턴

조건이 중첩될수록 코드는 오른쪽으로 점점 밀려납니다. 이를 "화살표 안티패턴(Arrow Anti-Pattern)"이라고도 합니다.

중첩 if의 문제점:

package main

import "fmt"

func processUser(name string, age int, active bool) string {
if name != "" {
if age >= 18 {
if active {
return fmt.Sprintf("%s님, 환영합니다!", name)
} else {
return "비활성 계정입니다"
}
} else {
return "미성년자는 이용할 수 없습니다"
}
} else {
return "이름이 없습니다"
}
}

func main() {
fmt.Println(processUser("Alice", 25, true))
fmt.Println(processUser("", 25, true))
fmt.Println(processUser("Bob", 16, true))
fmt.Println(processUser("Carol", 25, false))
}

Early Return 패턴으로 개선:

Early return 은 조건이 맞지 않으면 즉시 함수를 빠져나오는 방식입니다. 중첩을 없애고 "정상 경로(happy path)"를 함수 끝에 한 번만 두는 패턴입니다.

package main

import "fmt"

func processUser(name string, age int, active bool) string {
// 실패 조건을 먼저 걸러낸다 (Guard Clause)
if name == "" {
return "이름이 없습니다"
}
if age < 18 {
return "미성년자는 이용할 수 없습니다"
}
if !active {
return "비활성 계정입니다"
}

// 모든 조건을 통과한 정상 경로
return fmt.Sprintf("%s님, 환영합니다!", name)
}

func main() {
fmt.Println(processUser("Alice", 25, true))
fmt.Println(processUser("", 25, true))
fmt.Println(processUser("Bob", 16, true))
fmt.Println(processUser("Carol", 25, false))
}

실행 결과:

Alice님, 환영합니다!
이름이 없습니다
미성년자는 이용할 수 없습니다
비활성 계정입니다

두 코드는 동일한 결과를 출력하지만 Early Return 버전이 훨씬 읽기 쉽습니다. Go 커뮤니티에서는 Early Return 패턴을 강력히 권장합니다.


실전 예제: 에러 처리 패턴

Go에서 에러 처리는 if err != nil 패턴의 반복입니다. 실제 파일 읽기와 파싱을 처리하는 코드를 통해 패턴을 익혀봅니다.

package main

import (
"errors"
"fmt"
"strconv"
"strings"
)

// 사용자 데이터 구조
type User struct {
Name string
Age int
Email string
}

// CSV 한 줄을 파싱하는 함수
func parseUserLine(line string) (User, error) {
parts := strings.Split(line, ",")
if len(parts) != 3 {
return User{}, fmt.Errorf("필드 수 오류: 기대 3, 실제 %d", len(parts))
}

name := strings.TrimSpace(parts[0])
if name == "" {
return User{}, errors.New("이름은 비어 있을 수 없습니다")
}

age, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return User{}, fmt.Errorf("나이 파싱 실패: %w", err)
}
if age < 0 || age > 150 {
return User{}, fmt.Errorf("유효하지 않은 나이: %d", age)
}

email := strings.TrimSpace(parts[2])
if !strings.Contains(email, "@") {
return User{}, fmt.Errorf("유효하지 않은 이메일: %s", email)
}

return User{Name: name, Age: age, Email: email}, nil
}

func main() {
lines := []string{
"Alice, 30, alice@example.com",
"Bob, abc, bob@example.com", // 나이 오류
", 25, noname@example.com", // 이름 오류
"Carol, 28, carol-no-at", // 이메일 오류
"Dave, 22, dave@example.com",
}

for _, line := range lines {
if user, err := parseUserLine(line); err != nil {
fmt.Printf("파싱 실패 [%s]: %v\n", line, err)
} else {
fmt.Printf("파싱 성공: %+v\n", user)
}
}
}

실행 결과:

파싱 성공: {Name:Alice Age:30 Email:alice@example.com}
파싱 실패 [Bob, abc, bob@example.com]: 나이 파싱 실패: strconv.Atoi: parsing "abc": invalid syntax
파싱 실패 [, 25, noname@example.com]: 이름은 비어 있을 수 없습니다
파싱 실패 [Carol, 28, carol-no-at]: 유효하지 않은 이메일: carol-no-at
파싱 성공: {Name:Dave Age:22 Email:dave@example.com}

실전 예제: HTTP 상태코드 분기

package main

import (
"fmt"
"net/http"
"time"
)

type APIResponse struct {
StatusCode int
Body string
}

func handleResponse(resp APIResponse) error {
// 2xx 성공
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Printf("성공 (%d): %s\n", resp.StatusCode, resp.Body)
return nil
}

// 3xx 리다이렉트
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
fmt.Printf("리다이렉트 (%d): 새 URL로 이동 필요\n", resp.StatusCode)
return nil
}

// 4xx 클라이언트 에러
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("인증 필요: 로그인 후 다시 시도하세요")
}
if resp.StatusCode == http.StatusForbidden {
return fmt.Errorf("권한 없음: 접근이 거부되었습니다")
}
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("리소스를 찾을 수 없습니다")
}
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("요청 한도 초과: 잠시 후 다시 시도하세요")
}
return fmt.Errorf("클라이언트 에러: %d", resp.StatusCode)
}

// 5xx 서버 에러
if resp.StatusCode >= 500 {
return fmt.Errorf("서버 에러 (%d): 서버 측 문제입니다", resp.StatusCode)
}

return fmt.Errorf("알 수 없는 상태코드: %d", resp.StatusCode)
}

func simulateRequest(url string) APIResponse {
// 실제 HTTP 요청 대신 시뮬레이션
_ = url
_ = time.Now()
return APIResponse{StatusCode: 200, Body: "데이터 조회 성공"}
}

func main() {
responses := []APIResponse{
{StatusCode: 200, Body: "OK"},
{StatusCode: 301, Body: ""},
{StatusCode: 401, Body: ""},
{StatusCode: 403, Body: ""},
{StatusCode: 404, Body: ""},
{StatusCode: 500, Body: ""},
}

for _, resp := range responses {
if err := handleResponse(resp); err != nil {
fmt.Printf("에러: %v\n", err)
}
}
}

실행 결과:

성공 (200): OK
리다이렉트 (301): 새 URL로 이동 필요
에러: 인증 필요: 로그인 후 다시 시도하세요
에러: 권한 없음: 접근이 거부되었습니다
에러: 리소스를 찾을 수 없습니다
에러: 서버 에러 (500): 서버 측 문제입니다

고수 팁

1. 불리언 조건 단순화

// 나쁜 예
if isActive == true {
// ...
}

// 좋은 예
if isActive {
// ...
}

// 나쁜 예
if isError == false {
// ...
}

// 좋은 예
if !isError {
// ...
}

2. 초기화 구문으로 스코프 최소화

// 나쁜 예 — err가 불필요하게 함수 스코프로 노출
err := doSomething()
if err != nil {
return err
}

// 좋은 예 — err는 if 블록 안에만 존재
if err := doSomething(); err != nil {
return err
}

3. 조건을 함수로 추출해 가독성 향상

// 나쁜 예
if user.Age >= 18 && user.IsVerified && !user.IsBanned && user.Subscription != "free" {
grantAccess()
}

// 좋은 예
func canAccess(user User) bool {
return user.Age >= 18 &&
user.IsVerified &&
!user.IsBanned &&
user.Subscription != "free"
}

if canAccess(user) {
grantAccess()
}

4. errors.Is / errors.As와 조건문 조합

import "errors"

var ErrNotFound = errors.New("not found")

if err := fetchData(); err != nil {
if errors.Is(err, ErrNotFound) {
// 특정 에러 타입 처리
return defaultValue, nil
}
return nil, err // 다른 에러는 상위로 전파
}