Switch 문 — 값이나 타입으로 여러 갈래 중 하나를 고르는 선택문

Switch 문 — 값이나 타입으로 여러 갈래 중 하나를 고르는 선택문

컴퓨터는 보통 위에서 아래로 하나씩 실행해 나가는데요, 어떤 조건에 따라 여러 갈래 중 정확히 하나를 골라 실행하고 싶을 때가 있어요. 그럴 때 손에 쥐고 있는 게 바로 switch 문이에요. "switch" 문은 다중 분기(multi-way) 실행을 제공해요. switch 안에 있는 각 "케이스(case)"에 표현식이나 타입을 비교해서, 어느 분기(branch)를 실행할지를 정하는 구조죠.

출처: Go Specification

본문

switch 문은 문법적으로 크게 두 갈래로 나뉘어요. 문법을 먼저 한눈에 볼게요.

SwitchStmt = ExprSwitchStmt | TypeSwitchStmt .

괄호를 벗기면 **표현식 스위치(expression switch)**와 타입 스위치(type switch) 두 가지 형태가 있어요.

표현식 스위치는 케이스에 표현식을 넣어서, 그 표현식을 switch 표현식의 과 비교해요. 반면 타입 스위치는 케이스에 타입을 넣어서, 특별히 표시된 switch 표현식의 타입과 비교하죠. 여기서 중요한 점 하나는, switch 표현식은 switch 문 안에서 딱 한 번만 평가(evaluate) 된다는 거예요.

표현식 스위치 (Expression switches)

표현식 스위치의 동작을 순서대로 따라가 볼게요.

  1. switch 표현식을 평가해요.
  2. 케이스 표현식들은 좌에서 우로(왼쪽에서 오른쪽으로), 또 위에서 아래로 차례로 평가돼요. 굳이 상수일 필요는 없어요.
  3. 가장 먼저 switch 표현식의 값과 같은 케이스가 그 분기의 문장들을 실행해요. 나머지 케이스는 건너뛰어요.
  4. 어떤 케이스도 맞지 않는데 default 케이스가 있으면 그 문장들을 실행해요.

default 케이스는 최대 한 개만 둘 수 있고, switch 문 안 어디에 와도 상관없어요. 그리고 switch 표현식을 생략하면 그건 불리언 값 true와 같게 취급돼요. 즉 switch { ... }switch true { ... }라고 적은 셈이에요.

표현식 스위치의 문법은 이렇게 생겼어요.

ExprSwitchStmt = "switch" [ SimpleStmt ";" ] [ Expression ] "{" { ExprCaseClause } "}" .
ExprCaseClause = ExprSwitchCase ":" StatementList .
ExprSwitchCase = "case" ExpressionList | "default" .

중간에 SimpleStmt ";"가 들어가는 걸 눈여겨볼게요. switch 표현식 앞에는 간단한 문장(simple statement)을 하나 붙일 수 있는데, 그 문장이 표현식이 평가되기 전에 먼저 실행돼요. 뒤에서 예제로 확인해 볼게요.

switch 표현식 자체에 몇 가지 규칙이 적용돼요.

  • switch 표현식이 untyped 상수로 평가되면, 먼저 자신의 기본 타입(default type)으로 암시적으로 변환(convert)돼요.
  • 미리 선언된 untyped 값 nil은 switch 표현식으로 쓸 수 없어요.
  • switch 표현식의 타입은 반드시 비교 가능(comparable)해야 해요. 그래야 값끼리 같음을 비교할 수 있거든요.

케이스 표현식 쪽도 규칙이 있어요.

  • 케이스 표현식이 untyped면, 먼저 switch 표현식의 타입으로 암시적으로 변환돼요.
  • 변환된 각 케이스 표현식 x와 switch 표현식의 값 t에 대해, x == t가 유효한 비교(comparison)여야 해요.

쉽게 풀어보면, switch 표현식은 마치 타입을 명시하지 않은 임시 변수 t를 선언하고 초기화한 것처럼 다뤄져요. 그리고 각 케이스 표현식 x가 바로 그 t의 값과 같은지(equality) 검사하는 셈이에요.

케이스 끝에 fallthrough를 붙일 수 있다는 점도 기억해 두세요. case나 default 절에서 마지막 비어 있지 않은 문장은 (레이블이 붙을 수 있는) ["fallthrough" 문](#Fallthrough_statements)일 수 있어요. 그러면 이 절의 끝에서 제어 흐름이 **다음 절의 첫 문장**으로 흘러가요. 그게 아니면 제어 흐름은 switch 문의 끝으로 흘러가죠. 단, fallthrough`는 표현식 스위치에서 마지막 절을 제외한 모든 절의 마지막 문장으로만 올 수 있어요. (타입 스위치에서는 쓸 수 없어요 — 뒤에서 다시 다룰게요.)

switch 표현식 앞에 간단한 문장을 둘 수 있다고 했는데, 실제 코드로 확인해 볼게요.

switch tag {
default: s3()
case 0, 1, 2, 3: s1()
case 4, 5, 6, 7: s2()
}

switch x := f(); {  // missing switch expression means "true"
case x < 0: return -x
default: return x
}

switch {
case x < y: f1()
case x < z: f2()
case x == 4: f3()
}

두 번째 블록에 눈길이 가요. switch x := f(); {처럼 f()를 호출한 뒤 세미콜론으로 끊고 switch 표현식을 생략했는데요, 이러면 x := f()가 간단한 문장으로 먼저 실행되고, switch 표현식은 true로 취급돼요. 위 세 예제가 각각 어떤 case가 걸릴지 하나씩 따라가 보면 실행 흐름이 바로 보일 거예요.

구현상 제약도 하나 있어요. 컴파일러는 여러 케이스 표현식이 같은 상수로 평가되는 걸 허용하지 않을 수 있어요. 예를 들어 현재 컴파일러들은 case 표현식에 중복된 정수 상수, 부동소수점 상수, 문자열 상수를 허용하지 않아요.

타입 스위치 (Type switches)

타입 스위치는 값이 아니라 타입을 비교한다는 점만 다르고, 그 외에는 표현식 스위치와 비슷해요. 타입 스위치를 구분 짓는 건 특별한 switch 표현식의 형태예요. 실제 타입 대신 type 키워드를 쓰는 타입 단언(type assertion) 형태로 적어요.

switch x.(type) {
// cases
}

그러면 각 case가 실제 타입 T를 표현식 x의 **동적 타입(dynamic type)**과 대조해요. 타입 단언과 마찬가지로, x인터페이스 타입이어야 하고 타입 매개변수(type parameter)는 안 돼요. 그리고 case에 나열된 각 비-인터페이스 타입 Tx의 타입을 구현(implement)해야 해요. 또 타입 스위치의 case에 나열된 타입들은 전부 서로 다른(different) 타입이어야 해요.

타입 스위치의 문법은 이렇게 생겼어요.

TypeSwitchStmt  = "switch" [ SimpleStmt ";" ] TypeSwitchGuard "{" { TypeCaseClause } "}" .
TypeSwitchGuard = [ identifier ":=" ] PrimaryExpr "." "(" "type" ")" .
TypeCaseClause  = TypeSwitchCase ":" StatementList .
TypeSwitchCase  = "case" TypeList | "default" .

TypeSwitchGuard짧은 변수 선언(short variable declaration)을 포함할 수 있어요. 이 형태를 쓰면 그 변수는 각 절의 암시적 블록(implicit block) 안에서, TypeSwitchCase가 끝나는 지점에 선언돼요. case에 타입이 정확히 하나만 나열되어 있으면 변수는 그 타입을 갖고, 그렇지 않으면 TypeSwitchGuard에 있는 표현식의 타입을 가져요.

타입 대신 미리 선언된 식별자 nil을 쓸 수도 있어요. 이 nil case는 TypeSwitchGuard의 표현식이 nil 인터페이스 값일 때 선택돼요. 그리고 nil case는 최대 한 개만 둘 수 있어요.

타입 스위치가 실제로 어떻게 동작하는지, interface{} 타입의 표현식 x를 예로 들어볼게요.

switch i := x.(type) {
case nil:
	printString("x is nil")                // type of i is type of x (interface{})
case int:
	printInt(i)                            // type of i is int
case float64:
	printFloat64(i)                        // type of i is float64
case func(int) float64:
	printFunction(i)                       // type of i is func(int) float64
case bool, string:
	printString("type is bool or string")  // type of i is type of x (interface{})
default:
	printString("don't know the type")     // type of i is type of x (interface{})
}

주석에 i의 타입이 어떻게 달라지는지 적혀 있는 게 보이죠. 여기서 case에 타입이 하나만 있으면(예: int) i는 그 타입이 되고, 여러 개(예: bool, string)거나 nil·defaulti는 원래 x의 타입인 interface{}를 유지해요.

이 타입 스위치는 사실 아래처럼 일반 if 문으로 다시 쓸 수도 있어요. 타입 스위치가 내부적으로 무엇을 하는지 훨씬 잘 드러나는 코드라 같이 봐 두면 좋아요.

v := x  // x is evaluated exactly once
if v == nil {
	i := v                                 // type of i is type of x (interface{})
	printString("x is nil")
} else if i, isInt := v.(int); isInt {
	printInt(i)                            // type of i is int
} else if i, isFloat64 := v.(float64); isFloat64 {
	printFloat64(i)                        // type of i is float64
} else if i, isFunc := v.(func(int) float64); isFunc {
	printFunction(i)                       // type of i is func(int) float64
} else {
	_, isBool := v.(bool)
	_, isString := v.(string)
	if isBool || isString {
		i := v                         // type of i is type of x (interface{})
		printString("type is bool or string")
	} else {
		i := v                         // type of i is type of x (interface{})
		printString("don't know the type")
	}
}

x정확히 한 번만 평가된다는 점(v := x)과, 각 분기가 if 문으로 어떻게 짝을 이루는지 흐름을 따라가 보면서 읽어 보세요. 그러면 타입 스위치가 편의 문법(syntax sugar)이라는 느낌이 확 와요.

case의 타입 자리에 타입 매개변수(type parameter)제네릭 타입(generic type)을 쓸 수도 있어요. 만약 인스턴스화(instantiation)를 했을 때 그 타입이 switch 안의 다른 항목과 중복되는 걸로 판명나면, 가장 먼저 일치하는 case가 선택돼요.

func f[P any](x any) int {
	switch x.(type) {
	case P:
		return 0
	case string:
		return 1
	case []P:
		return 2
	case []byte:
		return 3
	default:
		return 4
	}
}

var v1 = f[string]("foo")   // v1 == 0
var v2 = f[byte]([]byte{})  // v2 == 2

f[string]("foo")에서는 Pstring으로 인스턴스화되니 case string이 아니라 case P가 먼저 와서 v1 == 0이 되고, f[byte]([])byte[]bytecase []P(즉 []byte)로 인스턴스화되지만 case []byte가 그 뒤에 있으니 실제로는 case []P가 먼저 걸려서 v2 == 2가 되는 식이에요.

마지막으로 타입 스위치에도 표현식 스위치와 똑같이, 타입 스위치 가드 앞에 간단한 문장을 붙일 수 있어요. 그 문장은 가드(guard)가 평가되기 전에 실행돼요. 그리고 type 스위치에서는 fallthrough 문을 쓸 수 없어요 — 앞에서 말한 대로, 이 점은 표현식 스위치와 다른 규칙이에요.

더 알아보기