Язык Go - switch
Switch в Go без fall-through по умолчанию, с условиями вместо значений, метками для выхода из цикла и type switch для работы с интерфейсами.
Содержание
Switch в Go выглядит знакомо — но работает иначе, чем в C, Java или JavaScript. Без автоматического провала в следующий case, с условиями вместо значений и с отдельной формой для типов. Давайте по порядку.
Базовая форма и отсутствие fall-through
В C-подобных языках если не написать break — выполнение «проваливается» в следующий case. В Go всё наоборот: каждый case завершается сам, break писать не нужно. Это убирает целый класс ошибок.
func dayType(day string) string {
switch day {
case "Saturday", "Sunday": // несколько значений через запятую — одно условие
return "выходной"
case "Monday":
return "понедельник, тяжёлый день"
default:
return "рабочий день"
}
}
Несколько значений в одном case — через запятую. Без дублирования кода, без fall-through.
Если fall-through всё-таки нужен — есть явное ключевое слово fallthrough. Встречается редко и обычно сигнализирует, что стоит пересмотреть структуру кода.
Switch без выражения — замена длинным if-else
Если убрать выражение после switch, Go считает, что переключаемся на true. Это позволяет писать условия прямо в case:
func unhex(c byte) byte {
switch { // нет выражения — switch on true
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
Это прямая замена цепочке if-else if-else if. Читается чище, особенно когда условий больше трёх. В Go такой стиль считается идиоматичным.
Ещё пример — функция, которая решает, надо ли экранировать символ в URL:
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
break внутри switch — осторожно
break завершает switch, а не внешний цикл. Это неочевидно и может сломать логику.
Представьте: вы парсите байты в цикле, внутри — switch. Нужно прервать именно цикл при ошибке:
Loop: // метка на цикле
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break // выходим из switch, цикл продолжается
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop // выходим из цикла по метке
}
if validateOnly {
break // выходим только из switch
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
break без метки — выход из switch. break Loop — выход из цикла с меткой Loop. Это разные вещи. Перепутать легко, компилятор не предупредит — оба варианта синтаксически корректны.
continue с меткой тоже работает, но только для циклов — к switch он не применим.
Практический пример: сравнение слайсов
Посмотрим, как switch без выражения используется в реальной задаче. Функция лексикографического сравнения двух байтовых слайсов:
// Compare возвращает:
// 0, если a == b
// -1, если a < b
// +1, если a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch { // сравниваем побайтово
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
// если равны — идём дальше
}
}
// все общие байты совпали — смотрим на длину
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
Два отдельных switch вместо вложенных if — и логика читается линейно сверху вниз.
Type switch: узнать тип из интерфейса
Отдельная форма switch — для работы с интерфейсами. Когда переменная имеет тип interface{} (или любой другой интерфейс), её конкретный тип неизвестен на этапе компиляции. Type switch позволяет его определить:
var t interface{}
t = functionOfSomeType()
switch t := t.(type) { // t.(type) — специальный синтаксис только внутри switch
default:
fmt.Printf("неизвестный тип %T\n", t) // %T — печатает тип переменной
case bool:
fmt.Printf("булево: %t\n", t) // здесь t имеет тип bool
case int:
fmt.Printf("целое: %d\n", t) // здесь t имеет тип int
case *bool:
fmt.Printf("указатель на bool: %t\n", *t) // здесь t имеет тип *bool
case *int:
fmt.Printf("указатель на int: %d\n", *t) // здесь t имеет тип *int
}
Обратите внимание на switch t := t.(type) — здесь переменная t переобъявляется. Снаружи t — это interface{}. Внутри каждого case — уже конкретный тип. Одно имя, разные типы в разных ветках. Выглядит странно, но это идиоматичный Go.
Где это реально нужно
Type switch встречается при работе с JSON, когда структура заранее неизвестна:
func describe(v interface{}) string {
switch val := v.(type) {
case nil:
return "null"
case bool:
if val {
return "true"
}
return "false"
case float64:
return fmt.Sprintf("число: %g", val) // json.Unmarshal числа → float64
case string:
return fmt.Sprintf("строка: %q", val)
case []interface{}:
return fmt.Sprintf("массив из %d элементов", len(val))
case map[string]interface{}:
return fmt.Sprintf("объект с %d полями", len(val))
default:
return fmt.Sprintf("что-то неожиданное: %T", val)
}
}
Ловушка с несколькими типами в одном case
Если перечислить несколько типов через запятую — переменная внутри case остаётся interface{}, а не конкретным типом:
switch v := x.(type) {
case int, int64: // оба целочисленных
fmt.Println(v) // v здесь interface{}, не int и не int64
// нельзя написать v + 1 — компилятор не разрешит
}
Это логично: Go не знает, какой именно тип использовать для v. Если нужно работать со значением — разделяйте на отдельные case.
Итоги
- Fall-through в Go нет по умолчанию — каждый
caseзавершается сам - Несколько значений в одном
case— через запятую - Switch без выражения — это switch on
true, замена цепочкеif-else if breakвнутри switch прерывает switch, не цикл — для цикла нужна меткаt.(type)работает только внутриswitch, не как отдельное выражение- В type switch с несколькими типами в одном
caseпеременная остаётсяinterface{}