switch в Go: условия, несколько case и fallthrough

• 7 мин чтения

switch в Go выбирает ветку по значению или условию. Несколько значений для одной ветки записывают через запятую в case. После выполнения ветки switch завершается сам; перейти в следующую можно явно, через fallthrough.

Первый пример можно сохранить в main.go и запустить целиком. В следующих фрагментах опущены package main, импорт fmt и функция main. Если не указано иначе, размещайте фрагменты внутри main и запускайте по отдельности.

switch по значению: case и default

Допустим, программа получает команду. Для start и run действие одинаковое:

package main

import "fmt"

func main() {
	command := "run"

	switch command {
	case "start", "run":
		fmt.Println("Запускаем задачу")
	case "stop":
		fmt.Println("Останавливаем задачу")
	default:
		fmt.Println("Неизвестная команда:", command)
	}
}

Сохраните код в main.go и запустите:

$ go run main.go
Запускаем задачу

Выражение после switch задаёт значение для сравнения. В примере это строка command. В каждом case перечислены значения, с которыми её сравнивают через ==.

Ветка default выполняется, если совпадений нет. Она необязательна: без неё при неизвестной команде программа просто продолжит работу после switch. Разрешена только одна ветка default. Её можно поставить в любом месте, но в конце она привычнее для чтения.

Писать break в конце каждого case не нужно. В программе с command := "run" выполняется только первая ветка. До case "stop" и default выполнение не доходит. Такой порядок описан в Effective Go.

Несколько значений в case и пустая ветка

Если разным значениям соответствует один обработчик, объедините их:

extension := ".jpeg"

switch extension {
case ".jpg", ".jpeg", ".png":
	fmt.Println("Изображение")
case ".txt", ".md":
	fmt.Println("Текст")
default:
	fmt.Println("Другой формат")
}

Пример выведет Изображение. Запятая разделяет альтернативные значения. Совпадения хотя бы с одним из них достаточно для выполнения ветки.

Соседние пустые case работают иначе:

extension := ".jpg"

switch extension {
case ".jpg":
case ".jpeg":
	fmt.Println("Изображение")
}
fmt.Println("После switch")

Здесь выводится только После switch. Значение совпало с ".jpg", но тело этой ветки пустое. Перехода к ".jpeg" не происходит. Для общего тела используйте case ".jpg", ".jpeg":.

switch без выражения: условия и диапазоны

Для диапазонов удобно использовать форму switch { ... }. В ней каждый case содержит логическое условие:

score := 85

switch {
case score < 0 || score > 100:
	fmt.Println("Оценка вне диапазона")
case score >= 90:
	fmt.Println("Отлично")
case score >= 70:
	fmt.Println("Хорошо")
default:
	fmt.Println("Нужно повторить материал")
}

Вывод: Хорошо. Форма без выражения эквивалентна switch true: Go ищет первое условие, равное true. Такой вариант подходит для цепочки if / else if, когда нужно выбрать одну ветку. Effective Go приводит тот же приём.

Порядок условий важен. Для score := 95 истинны и score >= 90, и score >= 70. Сработает первое из них. Если поставить score >= 70 выше, оценка 95 попадёт в ветку Хорошо.

Проверку недопустимых значений тоже оставляем первой. Иначе score := 150 пройдёт проверку score >= 90 и получит результат Отлично.

Запятая допустима и здесь: case score < 0, score > 100: выбирает одну ветку при любом из двух истинных условий. Для границ диапазона запись с || обычно яснее. Для одновременного выполнения двух условий нужен &&, например case score >= 70 && score < 90:.

Если несколько независимых действий должны выполниться при разных условиях, напишите несколько if. Без fallthrough один switch не выполнит все подходящие ветки.

Объявление переменных перед проверкой

Переменную можно объявить прямо в заголовке switch. От выражения для сравнения её отделяют точкой с запятой:

switch command := "stop"; command {
case "stop":
	fmt.Println("Останавливаем задачу")
default:
	fmt.Println("Неизвестная команда:", command)
}

Здесь переменная command доступна внутри switch, включая его ветки. После закрывающей скобки она уже недоступна.

Форма без выражения полезна, когда результат функции нужно сразу проверить. Для следующего фрагмента дополнительно подключите пакет strconv:

input := "42"

switch n, err := strconv.Atoi(input); {
case err != nil:
	fmt.Println("Нужно целое число")
case n < 0:
	fmt.Println("Нужно неотрицательное число")
default:
	fmt.Println("Принято:", n)
}

Пример выведет Принято: 42. При input := "abc" выполнится ветка ошибки, при input := "-5" сработает проверка знака. Atoi вызывается один раз. Переменные n и err существуют только внутри switch.

Если результат преобразования нужен дальше, объявите его перед конструкцией. О возврате результата вместе с ошибкой есть отдельный разбор в статье о функциях в Go.

В каком порядке вычисляются case

Значение после switch вычисляется один раз. Выражения в case проверяются сверху вниз, а внутри списка через запятую слева направо. После первого совпадения остальные выражения не вычисляются. В case разрешены переменные и вызовы функций, поэтому порядок можно увидеть в выводе:

probe := func(label string, value int) int {
	fmt.Println(label)
	return value
}

switch probe("switch", 2) {
case probe("первое значение", 1), probe("второе значение", 2):
	fmt.Println("Совпадение")
case probe("следующая ветка", 3):
	fmt.Println("Другая ветка")
}

Программа напечатает:

$ go run main.go
switch
первое значение
второе значение
Совпадение

Строки следующая ветка не будет: соответствующий вызов probe пропущен. Это учебный пример, чтобы проследить порядок. Если функция в case пишет в файл или меняет счётчик, её вызов может не состояться. Действие, которое должно выполниться при любом результате проверки, вынесите перед switch.

fallthrough: переход без проверки следующего case

fallthrough принудительно передаёт управление в тело следующей ветки:

n := 1

switch n {
case 1:
	fmt.Println("Первая ветка")
	fallthrough
case 2:
	fmt.Println("Вторая ветка")
default:
	fmt.Println("Нет совпадений")
}

Вывод:

$ go run main.go
Первая ветка
Вторая ветка

Значение n равно 1, но выполняется и тело case 2. При переходе через fallthrough выражения следующей ветки не вычисляются и совпадение не проверяется. После второй ветки switch завершается: для следующего перехода понадобился бы ещё один fallthrough.

Ограничения у fallthrough такие:

Правила перехода заданы в спецификации Go.

Для одинаковой обработки значений обычно достаточно списка через запятую. Если в ветках есть общая часть, её можно вынести в функцию или после switch. Используйте fallthrough, когда намеренно нужно продолжить выполнение в следующем теле. Перестановка веток при этом изменит поведение программы.

break внутри switch и цикла

В конце ветки break избыточен, но он позволяет завершить switch раньше. Например, пропустить остаток ветки при выключенной настройке:

command := "send"
enabled := false

switch command {
case "send":
	if !enabled {
		fmt.Println("Отправка отключена")
		break
	}
	fmt.Println("Отправляем сообщение")
}
fmt.Println("После switch")

Вывод: Отправка отключена, затем После switch. До отправки выполнение не дойдёт.

Если switch вложен в for, обычный break внутри ветки завершает ближайший switch. Цикл продолжится. Чтобы выйти из внешнего цикла, используйте метку:

commands := []string{"run", "stop", "run"}

Loop:
for _, command := range commands {
	switch command {
	case "stop":
		break Loop
	default:
		fmt.Println("Команда:", command)
	}
}
fmt.Println("Цикл завершён")

Пример выведет Команда: run только один раз, затем Цикл завершён. Вторая команда завершает весь цикл. Такое применение меток показано в Effective Go.

continue внутри switch, вложенного в цикл, начинает следующую итерацию цикла. А return выходит из всей функции. Это разные границы выхода.

Type switch: когда выбирать по типу

У switch есть отдельная форма для проверки динамического типа значения в интерфейсе:

var value any = 42

switch v := value.(type) {
case int:
	fmt.Println("Целое число:", v+1)
case string:
	fmt.Println("Байт в строке:", len(v))
case nil:
	fmt.Println("Пустой интерфейс")
default:
	fmt.Printf("Другой тип: %T\n", v)
}

Результат: Целое число: 43. В ветке с единственным типом int переменная v имеет тип int, поэтому к ней можно прибавить единицу. В case string она имеет тип string.

В обычном switch value ветки содержат значения. В switch value.(type) они содержат типы. Запись .(type) разрешена только в заголовке переключателя типов. Подробности про интерфейсные значения и извлечение типов разобраны в статье об интерфейсах Go.

Частые вопросы

Почему не работает switch n с case n > 10?
Выражение n > 10 даёт логический результат. В форме switch n Go должен сравнить его с n через ==, но целое число нельзя сравнить с логическим значением. Для условий используйте switch { case n > 10: ... }, а для отдельных чисел форму switch n { case 10, 20: ... }. Многоточия здесь обозначают пропущенные тела веток.
Как отличить nil от пустого слайса через switch?

Используйте switch без выражения. Сначала проверьте равенство с nil, затем длину:

values := []int{1, 2}

switch {
case values == nil:
	fmt.Println("Слайс равен nil")
case len(values) == 0:
	fmt.Println("Слайс пуст")
default:
	fmt.Println("Элементов:", len(values))
}

Пример выведет Элементов: 2. Сравнение с nil для слайса разрешено. Здесь оно стоит первым, чтобы отличить nil от пустого слайса, у которого длина тоже равна нулю.

Почему компилятор сообщает duplicate case?
Повтор одной и той же строковой или числовой константы в case компилятор Go отклоняет. Например, case "run", "run": содержит дубликат. Ошибка будет и при повторе в разных ветках. Удалите повтор или объедините обработчики в одной ветке.

Итог

Используйте switch value, когда сравниваете одно значение с несколькими вариантами. Для диапазонов и составных условий подходит switch без выражения. Если у вариантов общее тело, перечислите значения через запятую в одном case.

Проверяйте порядок пересекающихся условий: выполняется первая подходящая ветка. fallthrough нужен для явного перехода в следующее тело без проверки его условия. Пустой case такого перехода не делает.


Об авторе: Александр Бруяко — руководитель разработки с опытом в backend, инфраструктуре и управлении командами.


Теги: