Перейти к содержимому

Лекция 3. Операторы и управление потоком выполнения

Цели лекции

После лекции студент должен:

  • использовать арифметические, сравнительные и логические операторы;
  • писать условия через if, else if и else;
  • применять короткую инструкцию перед условием;
  • создавать циклы всех основных форм с помощью for;
  • перебирать строки и коллекции через range;
  • использовать выражения и конструкции switch;
  • управлять выполнением через break и continue;
  • избегать бесконечных циклов и ошибок границ.

1. Арифметические операторы

Основные операции:

ОператорЗначениеПример
+сложениеa + b
-вычитаниеa - b
*умножениеa * b
/делениеa / b
%остатокa % b

Пример:

a := 17
b := 5
fmt.Println(a + b) // 22
fmt.Println(a - b) // 12
fmt.Println(a * b) // 85
fmt.Println(a / b) // 3
fmt.Println(a % b) // 2

При делении двух целых чисел результат также целый. Дробная часть отбрасывается:

fmt.Println(10 / 4) // 2
fmt.Println(float64(10) / float64(4)) // 2.5

Деление целой переменной на ноль вызывает panic во время выполнения. Деление целой константы на ноль обнаруживается компилятором.

2. Составные присваивания

count := 10
count += 5
count -= 2
count *= 3
count /= 2
count %= 4

Инкремент и декремент существуют только как отдельные инструкции:

count++
count--

Их нельзя использовать внутри выражения:

// total := count++ // ошибка
count++
total := count

Префиксных форм ++count и --count в Go нет.

3. Операторы сравнения

a == b // равно
a != b // не равно
a < b
a <= b
a > b
a >= b

Результат сравнения имеет тип bool:

age := 19
isAdult := age >= 18
fmt.Println(isAdult)

Строки сравниваются лексикографически по байтовому представлению UTF-8:

fmt.Println("Go" < "Python") // true

Не все типы можно сравнивать через ==. Например, slices и maps сравнимы только с nil. Правила сравнения коллекций будут рассмотрены позже.

4. Логические операторы

ОператорЗначение
&&логическое И
`
!отрицание
age := 20
hasTicket := true
canEnter := age >= 18 && hasTicket
fmt.Println(canEnter)

Операторы && и || используют короткое вычисление:

denominator := 0
if denominator != 0 && 100/denominator > 2 {
fmt.Println("Условие выполнено")
}

Правая часть не вычисляется, если результат уже известен. Поэтому деление на ноль в примере не выполняется.

5. Условие if

Скобки вокруг условия не нужны, фигурные скобки обязательны:

temperature := 18
if temperature < 0 {
fmt.Println("Мороз")
} else if temperature < 15 {
fmt.Println("Прохладно")
} else {
fmt.Println("Тепло")
}

Открывающая фигурная скобка находится на той же строке, что и условие. Это требование синтаксиса, а не только стиля.

Сравнение с Python

Python:

if temperature < 0:
print("Мороз")
elif temperature < 15:
print("Прохладно")
else:
print("Тепло")

Go:

if temperature < 0 {
fmt.Println("Мороз")
} else if temperature < 15 {
fmt.Println("Прохладно")
} else {
fmt.Println("Тепло")
}

Go не преобразует значение к bool автоматически:

name := "Анна"
// if name { } // ошибка
if name != "" {
fmt.Println(name)
}

6. Короткая инструкция в if

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

if length := len("Go"); length > 0 {
fmt.Println("Длина:", length)
}

Переменная length видна во всех ветках этого if, но недоступна после него:

if score := 82; score >= 90 {
fmt.Println("Отлично")
} else if score >= 75 {
fmt.Println("Хорошо")
} else {
fmt.Println("Нужно повторить тему")
}
// fmt.Println(score) // ошибка: score вне области видимости

Наиболее распространённый вариант — получение результата и ошибки:

if value, err := strconv.Atoi("42"); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Число:", value)
}

7. Тернарного оператора нет

В Go нет выражения condition ? a : b и аналога Python a if condition else b. Используется обычное условие:

age := 17
status := "несовершеннолетний"
if age >= 18 {
status = "совершеннолетний"
}

Такое решение занимает больше строк, но сохраняет один очевидный способ записи условной логики.

8. Единственный цикл for

В Go есть только ключевое слово for, но оно поддерживает несколько форм.

Классический счётчик

for i := 0; i < 5; i++ {
fmt.Println(i)
}

Три части:

  1. i := 0 выполняется один раз;
  2. i < 5 проверяется перед каждой итерацией;
  3. i++ выполняется после тела.

Аналог while

count := 3
for count > 0 {
fmt.Println(count)
count--
}

Бесконечный цикл

for {
// цикл продолжается до break, return или завершения программы
}

Бесконечные циклы используются в серверах, обработчиках очередей и интерактивных программах. У цикла должен быть понятный механизм завершения.

9. range

range перебирает элементы строки или коллекции.

Перебор диапазона целых чисел

В современной версии Go можно перебирать целое число:

for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}

Если нужен диапазон от 1 до 5:

for i := range 5 {
fmt.Println(i + 1)
}

Для произвольных начала и шага используется классическая форма for.

Перебор slice

languages := []string{"Go", "Python", "Swift"}
for index, language := range languages {
fmt.Println(index, language)
}

Если индекс не нужен:

for _, language := range languages {
fmt.Println(language)
}

Если значение не нужно:

for index := range languages {
fmt.Println(index)
}

Перебор строки

for index, symbol := range "Go, мир!" {
fmt.Printf("байтовый индекс=%d символ=%c\n", index, symbol)
}

index — смещение в байтах, а symbol имеет тип rune. Для многобайтных символов индексы могут увеличиваться не на единицу.

Перебор map

scores := map[string]int{
"Анна": 90,
"Борис": 82,
}
for name, score := range scores {
fmt.Println(name, score)
}

Порядок обхода map не определён. Нельзя строить логику или тесты, предполагая конкретный порядок.

10. break и continue

break немедленно завершает ближайший цикл:

for i := 1; i <= 100; i++ {
if i == 6 {
break
}
fmt.Println(i)
}

continue переходит к следующей итерации:

for i := 1; i <= 10; i++ {
if i%2 != 0 {
continue
}
fmt.Println(i)
}

Частое применение continue — раннее исключение неподходящих данных:

values := []int{10, -3, 0, 7}
for _, value := range values {
if value <= 0 {
continue
}
fmt.Println("Положительное значение:", value)
}

11. Метки

Вложенный break завершает только ближайшую конструкцию. Метка позволяет выйти из внешнего цикла:

search:
for row := 0; row < 3; row++ {
for column := 0; column < 3; column++ {
if row == 1 && column == 2 {
fmt.Println("Найдено:", row, column)
break search
}
}
}

Метки следует применять редко. Если вложенная логика становится сложной, её лучше вынести в функцию и завершать через return.

12. switch по значению

day := 3
switch day {
case 1:
fmt.Println("Понедельник")
case 2:
fmt.Println("Вторник")
case 3:
fmt.Println("Среда")
default:
fmt.Println("Неизвестный день")
}

В Go не требуется писать break после каждой ветки: переход к следующему case автоматически не происходит.

Несколько значений объединяются:

switch day {
case 6, 7:
fmt.Println("Выходной")
default:
fmt.Println("Рабочий день")
}

13. switch без выражения

switch без выражения эквивалентен последовательности логических условий:

score := 84
switch {
case score < 0 || score > 100:
fmt.Println("Некорректный балл")
case score >= 90:
fmt.Println("Отлично")
case score >= 75:
fmt.Println("Хорошо")
case score >= 60:
fmt.Println("Удовлетворительно")
default:
fmt.Println("Не зачтено")
}

Выполняется первая подходящая ветка, поэтому порядок условий важен.

14. Короткая инструкция в switch

switch length := len("Go"); length {
case 0:
fmt.Println("Пустая строка")
case 1:
fmt.Println("Один байт")
default:
fmt.Println("Байтов:", length)
}

Переменная length доступна только внутри switch.

15. fallthrough

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

level := 2
switch level {
case 1:
fmt.Println("Базовый доступ")
fallthrough
case 2:
fmt.Println("Расширенный доступ")
}

Обычно fallthrough не нужен. Общие варианты лучше объединять через запятую или выносить повторяющийся код в функцию.

16. Type switch

Интерфейсы будут подробно изучены позже. Сейчас важно узнать форму конструкции:

func describe(value any) {
switch v := value.(type) {
case int:
fmt.Println("Целое число:", v)
case string:
fmt.Println("Строка:", v)
case bool:
fmt.Println("Логическое значение:", v)
default:
fmt.Printf("Неизвестный тип %T\n", v)
}
}

Type switch выбирает ветку по динамическому типу значения. Использовать any вместо нормальной типизации только ради такой проверки не следует.

17. Ранний выход

В Go нет отдельного оператора guard, но применяется тот же принцип: ошибочная или граничная ситуация обрабатывается первой.

func printDiscount(age int) {
if age < 0 {
fmt.Println("Возраст не может быть отрицательным")
return
}
if age < 18 {
fmt.Println("Скидка: 20%")
return
}
fmt.Println("Скидка: 5%")
}

Ранний выход уменьшает вложенность и делает основной сценарий заметнее.

18. Практический пример: анализ результатов

package main
import "fmt"
func main() {
scores := []int{95, 82, 74, 61, 48, 103, -5}
validCount := 0
total := 0
for index, score := range scores {
if score < 0 || score > 100 {
fmt.Printf("Результат %d пропущен: %d\n", index, score)
continue
}
validCount++
total += score
switch {
case score >= 90:
fmt.Printf("%d: отлично\n", score)
case score >= 75:
fmt.Printf("%d: хорошо\n", score)
case score >= 60:
fmt.Printf("%d: удовлетворительно\n", score)
default:
fmt.Printf("%d: не зачтено\n", score)
}
}
if validCount == 0 {
fmt.Println("Нет корректных результатов")
return
}
average := float64(total) / float64(validCount)
fmt.Printf("Средний балл: %.2f\n", average)
}

Здесь объединены:

  • перебор slice;
  • проверка корректности;
  • ранний continue;
  • классификация через switch;
  • накопление суммы;
  • защита от деления на ноль;
  • явное преобразование к float64.

19. Типичные ошибки

Ошибка границы

for i := 0; i <= len(values); i++ { // последний индекс лишний
fmt.Println(values[i])
}

Правильно:

for i := 0; i < len(values); i++ {
fmt.Println(values[i])
}

Или проще:

for _, value := range values {
fmt.Println(value)
}

Бесконечный цикл

count := 5
for count > 0 {
fmt.Println(count)
// забыли count--
}

Неправильный порядок условий

switch {
case score >= 60:
fmt.Println("Зачтено")
case score >= 90:
fmt.Println("Отлично") // недостижимо для 90 и выше
}

Сначала проверяются более узкие и строгие условия.

Ожидание упорядоченного map

Порядок элементов map не гарантирован. Для стабильного вывода ключи сначала собирают в slice и сортируют.

Избыточная вложенность

Вместо нескольких вложенных условий полезно проверять ошибки и граничные случаи в начале, используя continue или return.

20. Вопросы для самопроверки

  1. Чем целочисленное деление отличается от вещественного?
  2. Почему count++ нельзя поместить внутрь выражения?
  3. Какие значения можно использовать в условии if?
  4. Где видна переменная из короткой инструкции if?
  5. Как записать цикл, аналогичный while?
  6. Что возвращает range при переборе slice?
  7. Что возвращает range при переборе строки?
  8. Гарантирован ли порядок обхода map?
  9. Почему после case обычно не нужен break?
  10. Для чего применяется switch без выражения?
  11. Чем отличаются break и continue?
  12. Когда ранний выход делает код понятнее?

Итоги

Go использует небольшой набор конструкций управления: if, универсальный цикл for, range и switch. Условия всегда имеют тип bool, автоматический переход между ветками switch отсутствует, а ранние return и continue помогают сохранять основной путь выполнения линейным и читаемым.