🦫 Самоучитель по Go для начинающих. Часть 5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы

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

Условные конструкции

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

В Go есть два оператора для создания условных конструкций – if-else и switch-case. Познакомимся с каждым из них подробнее.

Оператор if-else

Оператор if-else в Go

Оператор if-else в Go имеет стандартный для многих языков программирования синтаксис:

if условие {
  // блок кода в случае истинности условия
} else {
  // блок кода в случае ложности условия
}

С помощью оператора if-else можно, к примеру, проверить число на чётность:

num := 12
if num % 2 == 0 {
	fmt.Println("Число чётное")
} else {
	fmt.Println("Число нечётное")
}

Эту же конструкцию можно записать в более компактном виде, используя короткое объявление оператора if (if short statement):

if num := 12; num % 2 == 0 {
	fmt.Println("Число чётное")
} else {
	fmt.Println("Число нечётное")
}

Иногда в программе необходимо обработать несколько различных условий. В таких случаях на помощь приходит оператор else if, который проверяет условие в случае ложности предыдущего. Продемонстрируем работу else if на примере программы для определения типа числа, введенного с клавиатуры:

package main

import (
	"fmt"
)

func main() {
	var num int
	fmt.Scan(&num) // ввод числа
	if num > 0 {
		fmt.Println("Число положительное")
	} else if num < 0 {
		fmt.Println("Число отрицательное")
	} else {
		fmt.Println("Число равно нулю")
	}
}

Буквально конструкцию if-elseif-else можно воспринимать так:

если условие1 {
  блок кода в случае истинности условия1
} иначе если условие2 {
  блок кода в случае истинности условия2
} иначе {
  блок кода в случае ложности всех предыдущих условий
}

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

var a, b = 1, 2
if a%2 == 0 && b%2 == 0 {
	fmt.Println("Оба числа чётные")
} else if a%2 == 0 || b%2 == 0 {
	fmt.Println("Хотя бы одно число чётное")
} else {
	fmt.Println("Оба числа нечётные")
}

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

Например, с помощью вложенных условий можно реализовать простейшую систему аутентификации пользователя:

var login, pass string
fmt.Scan(&login)
if login == "Gosha" {
	fmt.Println("Логин верный. Введите пароль.")
	fmt.Scan(&pass)
	if pass == "ilovego" {
		fmt.Println("Пароль верный! Успешный вход!")
	} else {
		fmt.Println("Пароль неверный! Попробуйте еще раз.")
	}
} else {
	fmt.Println("Неверный логин")
}

В этом примере мы запрашиваем ввод логина и далее сравниваем введенное значение со строкой "Gosha". В случае соответствия логина выводим сообщение и ожидаем ввод пароля, иначе выводим "Неверный логин". После проверки пароля на равенство строке "ilovego" печатаем сообщение либо об успешном входе, либо о неверном пароле.

👨‍💻 Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»
🎓 Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»
🧩 Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Обработка ошибок

В Go оператор if часто используется для обработки ошибок. Чтобы продемонстрировать это поведение, воспользуемся функцией strconv.Atoi, которая возвращает два значения – результат перевода строки в число и ошибку.

num, err := strconv.Atoi(“a”)
if err != nil {
  return err // один из вариантов обработки ошибки
}

Для проверки наличия ошибки используется оператор if, который сравнивает значение переменной err с нулевым (nil). Если err != nil, то это говорит о возникновении ошибки, которую нужно обработать в блоке оператора if.

Switch-case

Оператор switch-case выполняет те же задачи, что и if-else, но позволяет записывать условные выражения в более коротком виде. Он состоит из четырех ключевых слов: switch – для создания оператора, case – для проверки условий (аналог if), default – для задания значения по умолчанию в случае ложности предыдущих условий (аналог else), fallthrough – для принудительного выполнения следующего блока case.

Синтаксис switch-case выглядит следующим образом:

switch переменная {
case значение1:
	// блок кода, если переменная имеет значение1
case значение2:
	// блок кода, если переменная имеет значение2
...
default:
	// блок кода, если переменная не равна ни одному из проверенных значений
}

Пример применения switch-case с указанием переменной для проверки:

num := 0
switch num {
case 0:
	fmt.Println("Число равно нулю")
default:
	fmt.Println("Число не равно нулю")
}

Все операторы case выполняются последовательно. Но что если необходимо обязательно учесть определенное условие? В таком случае используется ключевое слово fallthrough. Оно ставится в конце блока case и указывает на то, что следующий оператор case будет выполнен независимо от истинности своего условия.

Пример switch-case без переменной для проверки и с ключевым словом fallthrough:

num := 3
switch {
case num < 5:
	fmt.Println("Число меньше пяти")
	fallthrough // если число меньше 5, то оно точно меньше 10, поэтому здесь можно указать fallthrough
case num < 10:
	fmt.Println("Число меньше десяти")
	fallthrough
case num < 100:
	fmt.Println("Число меньше ста")
default:
	fmt.Println("Число больше пяти")
}

В результате будет выведено:

Число меньше пяти
Число меньше десяти
Число меньше ста

Цикл for

Цикл for в Go

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

Основным элементом цикла является итератор – переменная, которая по заданному правилу изменяет свое значение.

В отличие от других языков, Go для создания цикла имеет только один оператор for. В общем случае его синтаксис выглядит так:

for инициализация итератора; условие для итератора; шаг итератора {
    // код в цикле
}

В качестве примера посчитаем квадраты чисел от 1 до 9 с использованием цикла:

for i := 1; i < 10; i++ {
	fmt.Println(i * i)
}

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

// вывод квадратов чисел от 10 до 1
for i := 10; i >= 0; i-- {
	fmt.Println(i * i)
} 

Шаг необязательно должен изменяться на единицу. Вместо этого можно использовать любое значение:

// вывод чётных чисел от 30 до 1
for i := 30; i > 0; i -= 2 {
	fmt.Println(i)
}

Как было упомянуто ранее, циклы часто используются для итерации по массивам. Это реализуется с использованием ключевого слова range, которое задает диапазон перебираемых значений:

array := []int{10, 20, 30, 40, 50}
for index, val := range array {
	fmt.Printf("Элемент с индексом %d: %d\\n", index, val) // форматированный вывод
}

Обратите внимание, что первой переменной в таком цикле является индекс, а второй – значение.

При объявлении цикла for необязательно указывать все параметры. Такая особенность была введена неслучайно, ведь это делает возможным реализацию второго типа циклов – while (цикл «пока»). Он выполняется до тех пор, пока заданное условие истинно.

Давайте перепишем предыдущий код для подсчета квадратов чисел от 1 до 9 в стиле цикла while:

i := 1
for i < 10 {
	fmt.Println(i * i)
	i++
}

При работе с таким циклом стоит следить за выполнением условия выхода и правильно изменять итератор, иначе есть риск получить бесконечный цикл, способный привести к зависанию или полному краху программы:

// ошибка: бесконечный цикл
for {
	fmt.Println("brrr")
}

Помимо проверки условия есть другой подход для выхода из цикла – оператор break. Он принудительно завершает выполнение цикла. Приведенный ниже код печатает квадраты чисел от 1 до 4 включительно, при этом break обеспечивает досрочный выход из for при достижении итератором числа 5:

for i := 1; i < 10; i++ {
	if i == 5 {
		break
	}
	fmt.Println(i * i)
} 

Несмотря на кажущуюся непригодность, бесконечные циклы бывают полезны для обеспечения бесперебойной работы программы. Чтобы это проиллюстрировать, напишем код для непрерывного считывания данных с консоли до ввода определённого символа:

for {
	var input string
	fmt.Scan(&input) // ввод данных
	if input == "q" { // условие выхода из цикла
		break
	}
}

Но если есть оператор, останавливающий цикл, то должен быть и продолжающий! И он есть – это оператор continue, досрочно переходящий к следующей итерации. Он бывает полезен для пропуска определенных значений. Например, код ниже выведет на экран квадраты всех чисел от 1 до 20, не делящихся на 3 или 7:

for i := 1; i <= 20; i++ {
	if i%3 == 0 || i%7 == 0 {
		continue // досрочный переход к следующей итерации цикла
	}
	fmt.Println(i * i)
}

Вложенные циклы

Циклы, как и условные конструкции, могут быть вложенными. Это полезный инструмент для генерации комбинаторных объектов и обработки многомерных структур данных, таких как n-мерные массивы, деревья и графы.

Простым примером вложенного цикла является программа для вывода на экран таблицы умножения чисел от 1 до 10:

for i := 1; i <= 10; i++ {
	for j := 1; j <= 10; j++ {
		fmt.Print(i*j, "\\t")
	}
	fmt.Println()
}

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

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

n, iterations := 100, 0
for i := 0; i < n; i++ {
	for j := 0; j < n; j++ {
		for k := 0; k < n; k++ {
			iterations++
		}
	}
}

fmt.Println("Количество итераций: ", iterations)

В примере выше количество операций будет 1003, а в общем случае говорят, что такая программа имеет сложность «порядка n в кубе» или O(n3). На маленьких значениях разница во времени выполнения может быть несущественна, но уже для n = 1000000 программа будет исполнятся критически долго, что недопустимо в условиях реальной разработки. Поэтому стоит избегать необдуманного использования вложенных циклов и оптимизировать их с помощью эффективных алгоритмов.

Для знакомства с понятием сложности алгоритмов рекомендуется прочитать статью Асимптотическая сложность алгоритмов: что за зверь?

Задачи

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

Задача 1: Гоша учится считать

Задача 1: Гоша учится считать

Первокласснику Гоше на уроке математики дали задание: посчитать сумму всех чисел от 20 до 500, которые делятся на 6, и при этом не делятся на 8. Помогите Гоше решить эту задачу.

Решение:

var sum int
for i := 20; i <= 500; i++ {
	if i % 6 == 0 && i % 8 != 0 {
		sum += i
	}
}
fmt.Println(sum)

Задача 2: Гоша изобретает калькулятор

Задача 2: Гоша изобретает калькулятор

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

Входные данные: в первой строке вводятся два целых числа, а на следующей – один из четырех возможных символов, обозначающих математическую операцию: +, -, *, /

Выходные данные: результат применения операции к числам, в случае неверного ввода возвращается сообщение: ««Ошибка, введите два целых числа и одну из четырех допустимых операций: +, -, *, /»

Решение: используем условную конструкцию switch-case

var a, b int
fmt.Scan(&a, &b)

var operation string
fmt.Scan(&operation)

switch operation {
case "+":
	fmt.Println(a + b)
case "-":
	fmt.Println(a - b)
case "*":
	fmt.Println(a * b)
case "/":
	fmt.Println(a / b)
default:
	fmt.Println("Ошибка, введите два целых числа и одну из четырех допустимых операций: +, -, *, /")
}

Задача 3: Гоша учится играть в шахматы

Задача 3: Гоша учится играть в шахматы

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

Входные данные: четыре числа от 1 до 8 – номер столбца и номер строки сначала для первой клетки, а потом для второй.

Выходные данные: "YES", если из первой клетки можно за один ход попасть во вторую, иначе "NO".

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

var x1, y1, x2, y2 int
fmt.Scan(&x1, &y1, &x2, &y2)
if x1 == x2 || y1 == y2 {
	fmt.Println("YES")
} else {
	fmt.Println("NO")
}

Задача 4: Гоша считает делители

Задача 4: Гоша считает делители

Гоше стало интересно, как можно найти сумму делителей заданного числа n. Давайте поможем ему решить эту задачу, написав алгоритм на Go.

Входные данные: целое число n.

Выходные данные: сумма делителей числа n.

Решение: достаточно в цикле проверить деление n на все числа от 1 до n / 2, не забыв учесть само число n.

var n, sum int
fmt.Scan(&n)
for i := 1; i < n/2+1; i++ {
	if n%i == 0 {
		sum += i
	}
}
sum += n // учитываем само число
fmt.Println(sum)

Задача 5: Гоша решает задачу со звездочкой

Задача 5: Гоша решает задачу со звездочкой

Гоше поручили написать код для детской игры FizzBuzz, правила которой следующие: игроки называют числа подряд, если число делится на 3, его заменяют на «Fizz», если делится на 5, то заменяют на «Buzz», а если делится на 3 и на 5 одновременно, то заменяют на «FizzBuzz». Ваша задача вывести все числа от 1 до 200 по правилам этой игры.

Решение: сразу оговоримся, что у этой задачи есть несколько решений. Самое очевидное заключается в рассмотрении всех возможных случаев, для каждого из которых выводится соответствующая строка. В представленном здесь решении используется похожий подход: каждую итерацию цикла создается строка, к которой приписывается определенное значение, зависящее от делимости итератора на 3 и 5. Если он не делится ни на одно из этих чисел, то к строке припишется строковое значение итератора.

for i := 1; i <= 200; i++ {
	var s string // заново инициализируем пустую строку
	if i%3 == 0 { 
		s += "Fizz"
	}
	if i%5 == 0 {
		s += "Buzz"
	}
	if s == "" { // если текущее число не делится на 3 и на 5
		s += fmt.Sprint(i) // записываем в строку текущее число
	}
	fmt.Println(s) // выводим строку
}

Подведём итоги

В этой части самоучителя по Go мы познакомились с фундаментальными понятиями в программировании – условными конструкциями и циклами. Их понимание позволит в дальнейшем создавать гибкие и функциональные программы.

В следующем уроке рассмотрим функции, аргументы, рекурсию и defer, а в конце по традиции закрепим теорию интересными задачами.

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)
  9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
  10. Наследование, абстракция, полиморфизм, инкапсуляция
  11. Обработка ошибок. Паника. Восстановление. Логирование
  12. Обобщенное программирование. Дженерики
  13. Работа с датой и временем. Пакет time
  14. Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
  15. Конкурентность. Горутины. Каналы
  16. Тестирование кода и его виды. Table-driven подход. Параллельные тесты
  17. Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
  18. Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
29 января 2017

Изучаем алгоритмы: полезные книги, веб-сайты, онлайн-курсы и видеоматериалы

В этой подборке представлен список книг, веб-сайтов и онлайн-курсов, дающих...