Управление потоком
Мы уверенно движемся вперёд. Можно только позавидовать - впереди у нас много нового, неизведанного и удивительного. Любознательность, в определённой степени бескорыстная - в ней сущность нашего продвижения по ступенькам науки. Приступим!
Под управлением потоком мы будем понимать операторы ветвления и циклы, которые мы сейчас и разберём.
Начинать будем с отличий. Первое и основное, что нужно понимать по данной теме в языке Go - это отсутствие начисто таких конструкций, как while, do while, foreach. Весь ассортимент сведен к одному единственному оператору - for. Во-вторых, следует запомнить, что, в отличие от большинства С-подобных языков, в языке Go параметры цикла for и условия операторов ветвления никогда не берутся в круглые скобки! И, наконец, нужно не забывать семантическое значение переноса строки, фактически, замещающее собою применение точки с запятой (semicolon). Именно поэтому, возможна только нижеприведенная форма записи:
оператор условие {
}
И будут всегда ошибочны любые другие формы:
//Такая:
оператор условие
{
}
//И вот такая:
оператор условие {}
Оператор for
Go - язык очень лаконичный и аккуратный. Поэтому отсутствие избыточности и разнообразия вполне в рамках его парадигмы. Отсутствие оператора while со товарищи с лихвой окупается полиморфностью оператора for.
Прежде всего, оператор for может принимать классическую форму:
package main
import "fmt"
func main() {
//круглые скобки не требуются!
for i := 0; i <= 10; i++ { //фигурная скобка в той же строке!
fmt.Println(i)
}
}
//Результат: вывод чисел от 0 до 10
Пожалуй, оператор for - один из немногих случаев массового применения точки с запятой (semicolon), тщательно избегаемой в языке Go.
Объявление переменной-счётчика можно при необходимости вынести за пределы конструкции, тем самым расширив её зону видимости. Не забудьте только сохранить точку с запятой в положенном ей месте.
package main
import "fmt"
func main() {
var i int = 0
for ; i <= 10; i++ {
fmt.Println(i)
}
fmt.Println(i) // 11
}
//Результат: вывод чисел от 0 до 10
//Переменная-счётчик i продолжает свой жизненный цикл, со значением 11
А вот так for может мимикрировать под цикл while:
package main
import "fmt"
func main() {
var i int = 0
for i <= 10 { //no semicolons
fmt.Println(i)
i++
}
}
//Результат: вывод чисел от 0 до 10
Обратите внимание, точки с запятой в данной ипостаси цикла for можно опустить, но их наличие не будет ошибкой.
И, наконец, бесконечный цикл:
package main
import "fmt"
func main() {
var i int = 0
for { //no semicolons
fmt.Println(i)
i++
if i > 10 {
break
}
}
}
//Результат: вывод чисел от 0 до 10
В данной конструкции точки с запятой также можно опустить, либо же использовать по желанию, чего я делать не рекомендую. Меньше знаков - чище код. Обратите внимание, нам пришлось использовать оператор break для выхода из бесконечного цикла. В случае использования оператора for в форме бесконечного цикла, это - единственная возможность из такого цикла выйти. При использовании подпрограмм Go (горутин), то есть многопоточности, зачастую используют бесконечные циклы, работа которых заканчивается только тогда, когда программа завершает свою работу. Рядом с оператором break стоит оператор continue, который прерывает лишь текущую итерацию цикла. При этом оператор for переходит к следующей итерации, не прерывая своей работы.
Выведем только чётные числа:
package main
import "fmt"
func main() {
for i := 0; i <= 10; i++ {
if i%2 != 0 {//если число нечётное
continue
}
fmt.Println(i)
}
}
//Результат: вывод только чётных чисел от 0 до 10
Операторы break и continue могут использоваться с метками, подобно оператору goto. Опишем синтаксис подобного явления. Для оживления изложения, построим так называемое "треугольное число" (последовательность A000217 в OEIS):
package main
import "fmt"
func main() {
var result int = 0
handle:
for i := 0; i < 10; i++ {
result = result + i
fmt.Println(result)
if result%2 == 1 {
continue handle
}
}
}
//Результат:
/*
0
1
3
6
10
15
21
28
36
45
*/
Циклы могут быть вложенными, к примеру при работе с многомерными массивами или при решении специфических задач.
Выведем, к примеру, таблицу Пифагора:
package main
import "fmt"
func main() {
for i := 1; i < 10; i++ {
for j := 1; j < 10; j++ {
fmt.Print(i*j, "\t")
}
fmt.Println()
}
}
//Результат:
/*
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
*/
Обратите внимание - \t - как и во множестве C-подобных языков - символ табуляции. Подобные символы, а также символы форматирования, так называемые "verbs" мы рассмотрим позднее в разделе, посвящённом работе с текстом.
И последнее о цикле for. При использовании с массивами, со срезами и отображениями, то есть с такими переменными, которые имеют свойство перечисляемости, цикл for имеет особую, дополнительную форму, которую можно в общем виде представить так:
for key, value := range enumarableVar{
fmt.Println(key, value)
Здесь range - ключевое слово. Имена же переменных key и value даны для наглядности и могут быть заменены любыми. Далее, для понимания происходящего, мы используем массив. Знаю, что не проходили. Да, обещаю вскоре исправиться.
package main
import "fmt"
func main() {
var arr [4]string = [4]string{"a", "b", "c", "d"}
fmt.Println("key\t value" )
for key, value := range arr{
fmt.Println(key, "\t", value)
}
}
//Результат:
/*
key value
0 a
1 b
2 c
3 d
*/
Key можно опустить:
package main
import "fmt"
func main() {
var arr [4]string = [4]string{"a", "b", "c", "d"}
fmt.Println("value")
for _, value := range arr {
fmt.Println(value)
}
}
//Результат:
/*
value
a
b
c
d
*/
Такова форма, если нужно опустить value:
package main
import "fmt"
func main() {
var arr [4]string = [4]string{"a", "b", "c", "d"}
fmt.Println("key")
for key := range arr {
fmt.Println(key)
}
}
//Результат:
/*
key
0
1
2
3
*/
Не станем спорить о пользе бесконечного цикла по диапазону; выглядит он таким образом:
package main
import "fmt"
func main() {
var arr [4]string = [4]string{"a", "b", "c", "d"}
for range arr {
fmt.Println("In FOR cycle...")
}
}
//Результат:
/*
In FOR cycle...
In FOR cycle...
In FOR cycle...
In FOR cycle...
*/
Операторы break и continue в цикле по диапазону остаются в прежней силе. Пожалуй, это исчерпывающая информация о цикле for.
Оператор goto
Коль скоро с оператором for мы расстались на ноте break и continue, перейдём по горячему следу к оператору goto. Как и в языке C, используется данный оператор совместно с метками. Синтаксис нехитрый:
handle:
//некое условие
goto handle
Существует тривиальное утверждение, что использование оператора goto - дурной тон программирования. Автор вполне согласен с данным утверждением. Это приём, ведущий к плохой читаемости кода и неочевидности ветвлений управления потоком, замысла программиста. Если вы использовали данный оператор - остановитесь, размыслите. Чаще всего, вы сможете таким образом "перефразировать" ваш код, что позволит в конечном счёте избавиться от goto. Однако, это довольно "прилипчивый" оператор, и иногда цена избавления от него - также неудобочитаемый и неясный код. To be or not to be - решение остаётся за вами. Приведу здесь вполне правдоподобный пример использования оператора goto. Пускай это будет программа "ping". Используя пакет стороннего разработчика github.com/sparrc/go-ping, программа производит опрос хоста ipAddr. В случае неудачи, программа повторяет это действие трижды.
package main
import (
"fmt"
"github.com/sparrc/go-ping"
)
func main() {
var rtrCounter = 0 //счётчик повторов
var ipAddr = "192.168.1.1"
pinger, err := ping.NewPinger(ipAddr)
if err != nil {
panic(err) //выход из программы!
}
pinger.SetPrivileged(true)
pinger.Count = 2
Retry:
pinger.Run()
stats := pinger.Statistics() //get send/receive/rtt stats
if stats.PacketLoss != 0 {
fmt.Println("ERROR!")
rtrCounter++
if rtrCounter <= 3 {
fmt.Println("Retrying... ", rtrCounter)
goto Retry
}
} else {
fmt.Println("SUCCESS!")
}
}
Для оживления изложения, добавлю сюда, пожалуй, пример решения задачки из leetcode. Задание таково:
Write a function to find the longest common prefix string amongst an array of strings. If there is no common prefix, return an empty string "".
Example 1:
Input: strs = ["flower","flow","flight"]
Output: "fl"
Example 2:
Input: strs = ["dog","racecar","car"]
Output: "" Explanation: There is no common prefix among the input strings.
Решение:
func longestCommonPrefix(strs []string) string {
prefix := strs[0]
start:
for _, str := range strs {
if !strings.HasPrefix(str, prefix) {
prefix = prefix[:len(prefix)-1]
goto start
}
}
return prefix
}
Решение достаточно оригинальное, и к тому же демонстрирует высокое сравнительное быстродействие.
If...else - неразлучная пара
Если излагать предмет в том порядке, как это обычно делают во всех пособиях, то именно с операторов if и else следовало начинать повествование в этой главе. Однако применение этих операторов в языке Go достаточно тривиально, что и натолкнуло автора на мысль начать рассказ именно с наиболее интересного - с ярких отличий. Однако, по ходу изложения, мы уже столь часто были вынуждены прибегнуть к использованию if...else, что разобрать данный предмет пришло, кажется, самое время.
Итак, приведём пример обычного использования оператора if:
package main
import "fmt"
func main() {
var flag = true
//круглые скобки не нужны!
if flag { //фигурная скобка в той же строке!
fmt.Println("Fizz!")
}
}
Как обычно, оператор if дополняет комплиметнарный ему оператор else:
package main
import "fmt"
func main() {
var flag = true
//круглые скобки не нужны!
if flag { //фигурная скобка в той же строке!
fmt.Println("Fizz!")
} else { //else в той же строке!
fmt.Println("Buzz!")
}
}
Следует отметить, что правило, касающееся семантического значения переносов строки, касается и употребления оператора else. Он сам, и его открывающая фигурная скобка, должны находиться в той же строке, что и закрывающая фигурная скобка предшествующего if. Все другие способы написания приведут вас к ошибке!
Как и во многих других языках, в Go допустима конструкция else if:
package main
import "fmt"
func main() {
x := 5
y := 5
if x > y {
fmt.Println("x > y")
} else if y > x {
fmt.Println("y > x")
} else {
fmt.Println("x = y")
}
}
//Результат:
//x = y
Блоков else if может быть в коде несколько.
Особым приёмом, отличительным для языка Go, является объявление переменных и использование выражений присваивания внутри оператора if:
package main
import "fmt"
func main() {
if ok := false; !ok {
fmt.Println("Ooops, something is wrong...")
}
}
//Результат:
//Ooops, something is wrong...
Обратите внимание на использование точки с запятой внутри такого выражения. Данная форма записи - очень экономная; она компактна, а также ограничивает область видимости и время существования объявленной таким образом переменной границами текущего блока кода. В уместном случае, рекомендуется пользоваться данным приёмом.
Необходимо сказать несколько слов об идеологии использования в языке Go операторов if...else. Повторим, что сердцевина этого языка - компактность и краткость. Именно поэтому, хорошим тоном программирования на Go считается так компоновать свой код, чтобы максимально сокращать все выражения, использовать как можно меньшее количество ветвлений, а в идеале - единственный оператор if.
Хорошо:
package main
import "fmt"
func main() {
var flag = true
if flag {
fmt.Println("Allright!")
} else {
fmt.Println("Ooops, something is wrong...")
}
}
А так лучше:
package main
import "fmt"
func main() {
var flag = true
if !flag {
fmt.Println("Ooops, something is wrong...")
return
}
fmt.Println("Allright!")
}
Также отметим, что чаще всего, цепочка ветвлений if...else той или иной длины чаще всего может быть заменена эквивалентным оператором switch. Перейдём к этому оператору.
Оператор switch
Особенностью оператора switch в языке Go, отличающей его, к примеру, от таких языков, как C/C++, C# и Java, является отсутствие так называемого "проваливания" от одного оператора case к другому. Закономерным следствием этого есть отсутствие необходимости использования оператора break в каждом кейсе. Как обычно, допускается и приветствуется использование заключительного default.
Пример обычного switch:
package main
import "fmt"
func main() {
var os = "Windows"
switch os {//открывающая скобка в той же строке!
case "Windows":
fmt.Println("Windows")
case "Linux":
fmt.Println("Linux")
default:
fmt.Println("Unsupported OS")
}
}
//Результат:
//Windows
Чтобы сработал default:
package main
import "fmt"
func main() {
var os = "FreeBSD"
switch os {
case "Windows":
fmt.Println("Windows")
case "Linux":
fmt.Println("Linux")
default:
fmt.Println("Unsupported OS")
}
}
//Результат:
//Unsupported OS
Приведём более сложный пример (аргументом case является предикат):
package main
import "fmt"
func main() {
var flag = true
var x = 0
switch flag {
case x > 0:
fmt.Println("x > 0")
case x < 0:
fmt.Println("x < 0")
default:
fmt.Println("Zero!")
}
}
//Результат:
//Zero!
Аргументом switch также может быть выражение:
package main
import "fmt"
func main() {
x := 2
switch x + 2 {
case 4:
fmt.Println("2+2=4")
case 5:
fmt.Println("2+2=5")
default:
fmt.Println("I don`t know")
}
}
//Результат:
//2+2=4
После оператора case можно указывать несколько значений, разделённых запятыми:
package main
import "fmt"
func main() {
x := 2
switch x {
case 0, 1, 2, 3:
fmt.Println("Число принадлежит диапазону 0...3")
default:
fmt.Println("Число не принадлежит заданному диапазону")
}
}
//Результат:
//Число принадлежит диапазону 0...3
Таким образом, мы рассмотрели все аспекты применения оператора switch.
Подытожим. На данный момент, мы изучили все операторы управления потоком в языке Go, за исключением оператора select, который является своеобразным аналогом оператора switch для так называемых каналов, к которым мы обратимся только при изучении многопоточности.
Раунд!
Last updated
Was this helpful?